llm_demo/static/index.js (258 lines of code) (raw):
/**
* Copyright 2023 Google, LLC
*
* Licensed under the Apache License, Version 2.0 (the `License`);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an `AS IS` BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {create_trace} from './trace.js';
// Records and keep track of the confirmation blocks (e.g. ticket booking confirmation)
let confirmations = {}
// Records and keep track of traces associated with each message
let traces = {}
// Submit chat message via click
$('.btn-group span').click(async (e) => {
await submitMessage();
});
// Submit chat message via enter
$(document).on("keypress", async (e) => {
if (e.which == 13) {
await submitMessage();
}
});
// Reset current user via click
$('#resetButton').click(async (e) => {
await reset();
});
// Event delegation for dynamic elements
document.addEventListener('click', function(event) {
const event_id = event.target.id;
if (event_id === 'chatuserImg') {
showSignOut();
}
if (event_id === 'signoutButton') {
signout();
}
if (event_id.startsWith('traceButton')) {
const trace_id = event_id.replace("traceButton", "");
showTrace(event, trace_id);
}
if (event_id.startsWith('confirmTicket')) {
const message_id = event_id.replace("confirmTicket", "");
confirmTicket(message_id);
}
if (event_id.startsWith('cancelTicket')) {
const message_id = event_id.replace("cancelTicket", "");
cancelTicket(message_id);
}
});
async function submitMessage() {
let msg = $('.chat-bar input').val();
// Add message to UI
logMessage("human", msg)
// Clear message
$('.chat-bar input').val('');
window.setTimeout(() => {
$('#loader-container').show();
$('.chat-content').scrollTop($('.chat-content').prop("scrollHeight"));
}, 400);
try {
// Prompt LLM
let answer = await askQuestion(msg);
$('#loader-container').hide();
// Add response to UI
if (answer.type === "message") {
logMessage("ai", answer.content, answer.trace)
} else if (answer.type === "confirmation") {
const messageId = generateRandomID(10);
buildConfirmation(answer.content, messageId)
}
} catch (err) {
window.alert(`Error when submitting question: ${err}`);
}
}
// Send request to backend
async function askQuestion(prompt) {
const response = await fetch('chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt }),
});
if (response.ok) {
const text_response = await response.text();
return JSON.parse(text_response)
} else {
console.error(await response.text())
return { type: "message", content: "Sorry, we couldn't answer your question 😢" }
}
}
async function reset() {
await fetch('reset', {
method: 'POST',
}).then(() => {
window.location.reload()
})
}
async function signout() {
await fetch('logout/google', {
method: 'POST',
}).then(() => {
window.location.reload()
})
}
function buildMessage(name, msg, trace_html) {
let traceobj = ""
if (!!trace_html) {
let traceid = generateRandomID(10);
traces[traceid] = trace_html
traceobj = `<div id="traceButton${traceid}" class="material-symbols-outlined info-icon">info</div>`
}
let image = ''
if (name === "ai"){
image = '<div class="sender-icon"><img src="static/logo.png"></div>'
} else if(name === "human" && $('.chat-user-image').first().attr('src')){
image = `<div class="sender-icon"><img src="${$('.chat-user-image').first().attr('src')}"></div>`
}
let message = `<div class="chat-bubble ${name}">
${image}
<span><div class="innermsg">${msg}</div>${traceobj}</span>
</div>`;
return message;
}
// Helper function to print to chatroom
function logMessage(name, msg, trace) {
let trace_html = undefined;
if (trace != null) {
trace_html = create_trace(trace);
}
let message = buildMessage(name, msg, trace_html);
$('.inner-content').append(message);
$('.chat-content').scrollTop($('.chat-content').prop("scrollHeight"));
}
function buildConfirmation(confirmation, messageId) {
if (["Insert Ticket","insert_ticket"].includes(confirmation.tool)) {
const params = confirmation.params;
const message_id = messageId;
confirmations[message_id] = params
const from = params.departure_airport;
const to = params.arrival_airport;
const flight = `${params.airline} ${params.flight_number}`;
const airline = params.airline;
const flight_number = params.flight_number;
const departure_time = params.departure_time;
const arrival_time = params.arrival_time;
const userName = $('#user-name').first().text();
const message = `<div class="chat-bubble ai" id="${message_id}">
<div class="sender-icon"><img src="static/logo.png"></div>
<div class="ticket-confirmation">
Please confirm the details below to complete your booking
<div class="ticket-header"></div>
<div class="ticket">
<div class="from">${from}</div>
<div class="material-symbols-outlined plane">travel</div>
<div class="to">${to}</div>
</div>
${buildBox('left', 133, 35, 15, "Departure", departure_time.replace('T', ' '))}
${buildBox('right', 133, 35, 15, "Arrival", arrival_time.replace('T', ' '))}
${buildBox('left', 205, 35, 15, "Flight", flight)}
${buildBox('left', 265, 35, 15, "Passenger", userName, "")}
${buildButton("Looks good to me. Book it!", 342, "#805e9d", "#FFF", "confirmTicket" + message_id)}
${buildButton("I changed my mind.", 395, "#f8f8f8", "#181a23", "cancelTicket" + message_id)}
</div></div>`;
$('.inner-content').append(message);
$('.chat-content').scrollTop($('.chat-content').prop("scrollHeight"));
}
}
function buildBox(place, top, left, size, type, value) {
let box = `<div style="top: ${top}px;position: absolute;${place}: ${left}px;font-size: ${size}px;">
<div style="font-size: 15px;margin-top: 0px;margin-left: 0px;margin-bottom: 3px;font-weight: bold;">
${type}
</div>
${value}
</div>`;
return box;
}
function buildButton(text, top, bg, color, element_id) {
let button = `<div class="button" id="${element_id}" style="border-radius: 5px;top: ${top}px;position: absolute;left: 10px;font-size: 15px;
background: ${bg};font-weight: bold;color: ${color};padding: 11px;width: calc(100% - 42px);
text-indent: 10px;box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;cursor: pointer;">
${text}
</div>`
return button;
}
function showTrace(event, id) {
const trace = traces[id];
const rect = event.target.getBoundingClientRect();
let leftPosition = rect.left + window.scrollX;
let topPosition = rect.bottom + window.scrollY;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (leftPosition + 500 > windowWidth) {
leftPosition = windowWidth - 500;
}
if (topPosition + 500 > windowHeight) {
topPosition = windowHeight - 500;
}
const tooltip = document.createElement('div');
tooltip.id = "trace"
tooltip.innerHTML = trace;
tooltip.style.left = `${leftPosition}px`;
tooltip.style.top = `${topPosition}px`;
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
overlay.addEventListener('click', function() {
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
}
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
});
document.body.appendChild(overlay);
document.body.appendChild(tooltip);
}
function removeTicketChoices(id) {
$(`#${id}`).find('.button').remove();
$(`#${id}`).find('.ticket-confirmation').height(325);
}
async function cancelTicket(id) {
const response = await fetch('book/flight/decline', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
logMessage("human", "I changed my mind.")
removeTicketChoices(id);
logMessage("ai", 'Booking declined. What else can I help you with?');
}
}
async function confirmTicket(id) {
logMessage("human", "Looks good to me.")
const params = JSON.stringify(confirmations[id]);
const response = await fetch('book/flight', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ params }),
});
if (response.ok) {
const text_response = await response.text();
removeTicketChoices(id);
logMessage("ai", "<p>Your flight has been successfully booked.</p>")
} else {
console.error(await response.text())
removeTicketChoices(id);
logMessage("ai", "Sorry, flight booking failed. 😢")
}
}
function generateRandomID(length) {
return Math.random().toString(36).substring(2, 2 + length);
}
function showSignOut(){
$('.popup-signout').show()
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
overlay.addEventListener('click', function() {
$('.popup-signout').hide()
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
});
document.body.appendChild(overlay);
}