const startTime = Date.now(); var conversation = null; class Conversation { constructor(name) { this.messages = []; this.name = romanize(name); } setInteractive(isInteractive) { const children = document.getElementById("textbox").children; for (let i = 0; i < children.length; i++) { children[i].disabled = !isInteractive; } } initialize(initialMessages) { document.title = this.name; document.getElementById("header-title").innerHTML = this.name; this.messages = initialMessages; } // for the user to send their own messages sendUserMessage(text) { const message = new UserMessage(text); message.updateStatus("sent"); setTimeout(() => { message.updateStatus("delivered"); this.render(); setTimeout(() => { message.updateStatus("read"); this.render(); }, 5000); }, 1000); this.messages.push(message); } // update the current HTML based on messages render() { // clear stale HTML getMessageList().innerHTML = ""; // if there are multiple 'read' messages, we only want the last one to display // the status. the other ones must have been read, so clear their statuses. let foundReadMessage = false; for (let i = this.messages.length - 1; i >= 0; i--) { const message = this.messages[i]; if (!message.getIsOurs()) continue; // if we haven't found a read message yet, let's check to see if we have now if (!foundReadMessage) { if (message.status == "read") { foundReadMessage = true; continue; } } else { // we have found a read message, which means all messages above it should // have empty status message.updateStatus(""); } } // render message elements for (let i = 0; i < this.messages.length; i++) { const messageRoot = document.getElementById("messages"); const newMessage = this.messages[i]; messageRoot.appendChild(newMessage.getElement()); } } } function getMessageList() { return document.getElementById("messages"); } function romanize(text) { text = text.replaceAll('u', 'v'); text = text.replaceAll('U', 'V'); return text; } class AgentMessage { constructor(text, senderName) { this.text = text; this.senderName = senderName; } getIsOurs() { return false; } getElement() { const liElem = document.createElement("li"); liElem.className = "message"; const contentElem = document.createElement("span"); contentElem.className = "message-content rounded-rectangle theirs"; liElem.appendChild(contentElem); if (this.senderName) { const nameElem = document.createElement("h3"); nameElem.innerHTML = romanize(this.senderName); contentElem.appendChild(nameElem); } const textElem = document.createElement("span"); textElem.className = "message-text"; textElem.innerHTML = romanize(this.text); contentElem.appendChild(textElem); return liElem; } } class UserMessage { constructor(text) { this.createdTime = Date.now(); this.text = romanize(text); this.status = ""; } getIsOurs() { return true; } getElement() { const liElem = document.createElement("li"); liElem.className = "message"; const contentElem = document.createElement("span"); contentElem.className = "message-content rounded-rectangle ours"; liElem.appendChild(contentElem); const textElem = document.createElement("span"); textElem.className = "message-text"; textElem.innerHTML = this.text; contentElem.appendChild(textElem); const statusElem = document.createElement("p"); statusElem.className = "message-status"; statusElem.innerHTML = this.status; liElem.appendChild(statusElem); return liElem; } updateStatus(newStatus) { this.status = newStatus; } } class SystemMessage { constructor(text) { this.text = romanize(text); } getIsOurs() { return false; } getElement() { const liElem = document.createElement("li"); liElem.className = "system-message"; liElem.innerHTML = this.text; return liElem; } } function setTypingIndicator(isTyping) { document.getElementById("typing-indicator").innerHTML = isTyping ? `${conversation.contactName} is typing...` : ""; } // add the message at the index to the displayed messages function addMessage(message) { getMessageList().innerHTML += message.getHtml(); // scroll as far as we can so that messages aren't hidden window.scrollTo(0, document.body.scrollHeight); } function updatePings() { const title = conversation.name; let newTitle = conversation.pings > 0 ? `(${conversation.pings}) ${title}` : title; document.title = newTitle; } function clearPings() { conversation.pings = 0; updatePings(); } // returns a decimal value between min and max function getRandomInRange(min, max) { const range = max - min; return min + Math.random() * range; } function updateChat(message) { addMessage(message); const previewText = conversation.getTypedMessageText(); document.getElementById("textbox-input").value = previewText; updatePings(); } function pressSendButton() { const textBox = document.getElementById("textbox-input"); if (event.type == "keydown" && event.key != "Enter") { textBox.value = romanize(textBox.value); return; } // we have interacted with the page so remove all pings clearPings(); // get the content of the text box const text = textBox.value; if (!text) return; textBox.value = ""; conversation.sendUserMessage(text); conversation.render(); // TODO: start process of receiving next message from server (or fake it for now) } function onMessageReceived(message) { updateChat(message); setTypingIndicator(false); } function onMessageSent(message) { updateChat(message); } // probably a bit hacky! but this saves having to do like, state or something in CSS? // which probably is possible and probably would be the better way to do it, but that // sounds like a bunch of learning i'm not SUPER in the mood for function setVisibleOnMobile(element, isVisible) { let classes = element.className.split().filter(c => c != ""); const invisibleClass = "invisible-on-mobile"; const visibleClass = "visible"; if (isVisible && !classes.includes(visibleClass)) { const idx = classes.indexOf(invisibleClass); if (idx != -1) { classes.splice(idx, 1); } classes.push(visibleClass); } else if (!isVisible && !classes.includes(invisibleClass)) { const idx = classes.indexOf(visibleClass); if (idx != -1) { classes.splice(idx, 1); } classes.push(invisibleClass); } element.className = classes.join(" "); } function showSidePanel() { // this function can only be called on mobile. the main conversation should be // hidden and the side conversations panel should take up the whole screen. const mainPanel = document.getElementById("main-panel"); const conversationListElem = document.getElementById("side-panel"); setVisibleOnMobile(mainPanel, false); setVisibleOnMobile(conversationListElem, true); } function readConversationJson(path, callback) { fetch(path) .then(response => response.json()) .then(json => callback(json)); } function showConversation(path) { const mainPanel = document.getElementById("main-panel"); const conversationListElem = document.getElementById("side-panel"); setVisibleOnMobile(mainPanel, true); setVisibleOnMobile(conversationListElem, false); readConversationJson(path, json => { conversation = new Conversation(json.title); const jsonMessages = json.messages; const participants = json.characters; let initialMessages = []; for (let i = 0; i < jsonMessages.length; i++) { const data = jsonMessages[i]; const text = data.text; if (data.character == -1) { const message = new SystemMessage(text); initialMessages.push(message); } else if (data.character == 0) { const message = new UserMessage(text); message.updateStatus("read"); initialMessages.push(message); } else { const message = participants.length > 2 ? new AgentMessage(text, participants[data.character]) : new AgentMessage(text); initialMessages.push(message); } } conversation.initialize(initialMessages); conversation.setInteractive(json.interactive); conversation.render(); }); } function addConversationPreview(path) { const listRoot = document.getElementById("side-panel"); readConversationJson(path, json => { const messages = json.messages; const elem = document.createElement("div"); elem.onclick = () => showConversation(path); elem.className = "conversation"; const headerElem = document.createElement("h2"); headerElem.innerHTML = romanize(json.title); elem.appendChild(headerElem); const previewElem = document.createElement("span"); previewElem.innerHTML = romanize(messages[messages.length - 1].text); elem.appendChild(previewElem); listRoot.appendChild(elem); }); } function populateConversationList() { const conversationFiles = [ "caesar.json", "lucius.json", "ides-of-march.json", "lepidus.json", "publius.json", "sextus.json" ]; for (let i = 0; i < conversationFiles.length; i++) { const path = conversationFiles[i]; addConversationPreview(path); } } setTypingIndicator(false); populateConversationList(); showConversation("ides-of-march.json");