├── LICENSE ├── README.md ├── docs ├── conversation.js ├── index.html ├── main.css ├── peekobot.css └── peekobot.js ├── images └── noun_chatbot_1585775.svg ├── peekobot.css └── peekobot.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peekobot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peekobot 2 | 3 | Peekobot is a simple, choice-driven chatbot framework for your website written in ~~less 4 | than~~ just over 100 lines of ES6 vanilla JavaScript (and some CSS). 5 | 6 | There is an [example bot](https://peekobot.github.io/peekobot/) you can see in the [`/docs`](/docs) folder. 7 | 8 | There is also a [CodePen](https://codepen.io/magicroundabout/pen/RwwXxoo) you can tinker with. 9 | 10 | ## Features 11 | 12 | * Small, simple, zero dependencies (unless you need old browser compatibility) 13 | * Define your conversation as a simple JavaScript object 14 | * Choice/button driven conversations 15 | * Options to link to URLs as well as other parts of the conversation 16 | 17 | ## Browser Compatibility 18 | 19 | I use async/await and CSS custom properties, so, broadly speaking, Internet Explorer 20 | and Opera Mini are not supported. 21 | 22 | You can use Babel or similar to bring IE11 compatibility to the JavaScript. 23 | 24 | You can also manually inline the CSS custom properties if you want to. 25 | 26 | ## Usage 27 | 28 | Peekobot is not a package or library. You can't `npm install` it. Think of it as a starter kit or framework. The idea is that you take a copy of it and do your own thing with it. 29 | 30 | If you do use it, please [drop me a line](https://twitter.com/magicroundabout/) and show me what you made! I'd love to see what others are doing with it. Thanks! 🙌 31 | 32 | To use Peekobot, you need to: 33 | 34 | 1. Add Peekobot scripts and styles to your HTML 35 | 2. Add Peekobot markup to your HTML 36 | 3. Define your conversation 37 | 38 | ### 1. Add Peekobot scripts and styles to your HTML 39 | 40 | Download the [`peekobot.js`](peekobot.js) and [`peekobot.css`](peekobot.css) files into your project. 41 | 42 | You can do this by grabbing the raw code for these files from GitHub or by cloning the 43 | project. 44 | 45 | Then add the Peekobot scripts and styles to your HTML. 46 | 47 | These should go in the `head`: 48 | 49 | ```html 50 | 51 | 57 | 58 | 59 | ``` 60 | 61 | Note that you can change the height of the chatbot window here and define an "avatar" 62 | URL to be used in the chat by your chatbot. This should be small, square and fit within a 63 | circle shape. My CSS displays at 24px square, so a 48px x 48px image should be fine. 64 | 65 | These should go at the bottom of your HTML to load the JavaScript: 66 | 67 | ```html 68 | 69 | 70 | ``` 71 | 72 | ### 2. Add Peekobot markup to your HTML body 73 | 74 | Add the Peekobot markup to your HTML body where you want the chatbot to appear: 75 | 76 | ```html 77 |
78 |
79 |
80 |
81 |
82 | 83 | ``` 84 | 85 | ### 3. Define your conversation 86 | 87 | The conversation definition should be placed in a JavaScript variable called `chat`. 88 | I define this in the `conversation.js` file. You can inline it if you want to. 89 | 90 | The `chat` variable should be an object. In the chat object: 91 | 92 | * the first property name/key should be the number 1 93 | * subsequent property names can be numbers or strings - strings allow you to group parts of your conversation 94 | * each property value is an entry in the conversation. 95 | 96 | A conversation entry should have: 97 | 98 | - A `text` property that is what the chatbot says at this point in the conversation 99 | - Either: 100 | - A `next` property, which defines the next chat entry by stating a numerical key 101 | of the chat object and is used when the chatbot needs to say several things 102 | in turn without input from the user 103 | OR 104 | - An `options` property that defines the choices a user can take. This is an 105 | array of option objects. 106 | 107 | An options object should have: 108 | 109 | - a `text` property that is the label for the user's choice 110 | AND EITHER 111 | - a `next` property that defines the next chat entry by stating a property key of 112 | the chat object and is used when the user selects this option 113 | OR 114 | - a `url` property that defines a link for the user to be taken to 115 | 116 | A simple example chat object is: 117 | 118 | ```js 119 | const chat = { 120 | 1: { 121 | text: 'Good morning sir', 122 | next: 'question1' 123 | }, 124 | question1: { 125 | text: 'Would you like tea or coffee with your breakfast?', 126 | options: [ 127 | { 128 | text: 'Tea', 129 | next: 'response1' 130 | }, 131 | { 132 | text: 'Coffee', 133 | next: 'response2' 134 | } 135 | ] 136 | }, 137 | response1: { 138 | text: 'Splendid - a fine drink if I do say so myself.' 139 | }, 140 | response2: { 141 | text: 'As you wish, sir' 142 | } 143 | } 144 | ``` 145 | 146 | ## Ensure utf-8 147 | 148 | To use emoji's and other extended characters it's a good idea to ensure you are specifying UTF-8. 149 | 150 | You are probably doing this anyway, or maybe your web server or CMS is doing this for you. But if not it's worth 151 | making sure you have the correct `meta` tag in your ``: 152 | 153 | ```html 154 | 155 | ``` 156 | 157 | ## Disclaimers 158 | 159 | This is my first proper open source project. It's kinda neat, and it works, but it's 160 | probably not finished. My main concerns are 161 | 162 | * Accessibility: I've not really looked at accessibility of this code. It probably needs some work 163 | * Security - it's entirely possible that some script could hijack the bot's script code. 164 | 165 | Let me know if you have ideas about how to fix these things by raising an issue. 166 | 167 | ## Peeko-what? 168 | 169 | I released this in a bit of a hurry and needed a name. It's a mash-up of: 170 | 171 | * picobot 172 | * peek-a-boo 173 | 174 | and I mostly chose it becase all the other "small bot" names, such as picobot, nanobot, etc were taken. It kinda works. 175 | 176 | ## Buy me a coffee 177 | 178 | If you like Peekobot, or if it's helped you in some way, feel free to [buy me a coffee](https://ko-fi.com/magicroundabout). 179 | 180 | ## Credits/Contributors 181 | 182 | * [Jesper Johansson](https://github.com/JeppeJ) 183 | -------------------------------------------------------------------------------- /docs/conversation.js: -------------------------------------------------------------------------------- 1 | /* The chat const defines the Peekobot conversation. 2 | * 3 | * It should be an object with numerical property names, and each property is an entry 4 | * in the conversation. 5 | * 6 | * A conversation entry should have: 7 | * - A "text" property that is what the chatbot says at this point in the conversation 8 | * - Either: 9 | * - A "next" property, which defines the next chat entry by stating a numerical key 10 | * of the chat object and is used when the chatbot needs to say several things 11 | * without input from the user 12 | * OR 13 | * - An "options" property that defines the choices a user can take this is an 14 | * array of option objects 15 | * 16 | * An options object should have: 17 | * - a "text" property that is the label for the user's choice 18 | * AND EITHER 19 | * - a "next" property that defines the next chat entry by stating a numerical key of 20 | * the chat object and is used when the user selects this option 21 | * OR 22 | * - a "url" property that defines a link for the user to be taken to 23 | * 24 | * A simple example chat object is: 25 | * const chat = { 26 | * 1: { 27 | * text: 'Good morning sir', 28 | * next: 2 29 | * }, 30 | * 2: { 31 | * text: 'Would you like tea or coffee with your breakfast?', 32 | * options: [ 33 | * { 34 | * text: 'Tea', 35 | * next: 3 36 | * }, 37 | * { 38 | * text: 'Coffee', 39 | * next: 4 40 | * } 41 | * ] 42 | * }, 43 | * 3: { 44 | * text: 'Splendid - a fine drink if I do say so myself.' 45 | * }, 46 | * 4: { 47 | * text: 'As you wish, sir' 48 | * } 49 | * } 50 | */ 51 | const chat = { 52 | 1: { 53 | text: 'Hi! Welcome to Peekobot.', 54 | options: [ 55 | { 56 | text: '👋', 57 | next: 2 58 | } 59 | ] 60 | }, 61 | 2: { 62 | text: 'Peekobot is a really simple, choice-driven chatbot framework made in less than just over 100 lines of vanilla JavaScript', 63 | next: 3 64 | }, 65 | 3: { 66 | text: 'But you probably knew that anyway.', 67 | options: [ 68 | { 69 | text: "Yes, I did!", 70 | next: 4 71 | }, 72 | { 73 | text: "Nope, this is news.", 74 | next: 5 75 | } 76 | ] 77 | }, 78 | 4: { 79 | text: 'Awesome. This chat is still in development. Happy coding!', 80 | }, 81 | 5: { 82 | text: 'Aah, you\'re missing out!', 83 | next: 6 84 | }, 85 | 6: { 86 | text: 'You should check it out on GitHub', 87 | options: [ 88 | { 89 | text: "Go to GitHub", 90 | url: "https://github.com/peekobot/peekobot" 91 | } 92 | ] 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Peekobot 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |

Peekobot

34 |

35 | Peekobot is a simple choice-driven chatbot framework in less than just over 100 lines of vanilla JS 36 |

37 |

38 | I'm made by Ross Wintle and 39 | you can find me on GitHub 40 |

41 |

Icon credit: "chat bot" by GJR from the Noun Project

42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --brand-black: hsl(197, 44%, 19%); 3 | --bot-height: 80vh; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | padding: 3rem 1rem; 11 | font-family: sans-serif; 12 | color: var(--brand-black); 13 | } 14 | 15 | @media (min-width: 800px) { 16 | body { 17 | max-width: 800px; 18 | margin: 0 auto; 19 | } 20 | } 21 | 22 | p { 23 | font-size: 1.2rem; 24 | line-height: 1.3; 25 | } 26 | 27 | .container { 28 | width: 100%; 29 | margin: 0; 30 | } 31 | 32 | .text-center { 33 | text-align: center; 34 | } 35 | 36 | /* Font sizes */ 37 | /* .xl { 38 | font-size: 3.5rem; 39 | } */ 40 | 41 | /* .large { 42 | font-size: 2rem; 43 | } */ 44 | 45 | /* Padding */ 46 | /* .reset-padding { 47 | padding: 0; 48 | } */ 49 | 50 | /* Margin */ 51 | /* .reset-margin { 52 | margin: 0; 53 | } */ 54 | 55 | /* .large-margin { 56 | margin-top: 2rem; 57 | } */ 58 | 59 | /* .medium-margin { 60 | margin-top: 1.5rem; 61 | } */ 62 | 63 | header { 64 | width: 100%; 65 | margin-bottom: 3rem; 66 | } 67 | 68 | main { 69 | width: 100%; 70 | height: 80vh; 71 | } 72 | 73 | header img { 74 | display: inline-block; 75 | width: 100%; 76 | max-width: 400px; 77 | height: auto; 78 | } 79 | 80 | h1 { 81 | font-family: 'Baloo Bhaina', sans-serif; 82 | line-height: 1.2; 83 | } 84 | 85 | h2, h3, h4 { 86 | font-family: 'Baloo Bhaina', sans-serif; 87 | line-height: 1.2; 88 | } 89 | 90 | @media (min-width: 768px) { 91 | 92 | body { 93 | display: flex; 94 | } 95 | 96 | header { 97 | width: 50vw; 98 | margin-right: 5vw; 99 | } 100 | 101 | main { 102 | width: 45vw; 103 | } 104 | 105 | .xl { 106 | font-size: 5rem; 107 | } 108 | 109 | .large { 110 | font-size: 3rem; 111 | } 112 | 113 | 114 | } 115 | -------------------------------------------------------------------------------- /docs/peekobot.css: -------------------------------------------------------------------------------- 1 | #peekobot-container { 2 | border: 1px solid hsl(0, 0%, 90%); 3 | border-radius: 12px; 4 | box-shadow: 2px 2px 5px hsl(0, 0%, 60%); 5 | padding: 1rem 1rem 2rem 1rem; 6 | height: var(--peekobot-height); 7 | position: relative; 8 | } 9 | #peekobot-inner { 10 | height: 100%; 11 | overflow-y: scroll; 12 | overflow-x: hidden; 13 | scroll-behavior: smooth; 14 | position: relative; 15 | /* Hiding scrollbars is a pain. This is from: https://redstapler.co/css-hidden-scrollbar-while-scrollable-element/ */ 16 | scrollbar-width: none; 17 | -ms-overflow-style: none; 18 | } 19 | #peekobot-inner::-webkit-scrollbar { 20 | display: none; /* Chrome Safari */ 21 | } 22 | 23 | .chat-response, 24 | .chat-ask { 25 | opacity: 0; /* Set to activated to show */ 26 | transform: translateY(-50%); /* Set to activated to move down */ 27 | transition: all 0.3s 0.3s; 28 | border-radius: 12px; 29 | background-color: hsl(0, 0%, 90%); 30 | padding: 0.5rem 0.7rem; 31 | line-height: 1.4; 32 | color: black; 33 | width: 80%; 34 | margin-bottom: 0.5rem; 35 | } 36 | .chat-response { 37 | margin-left: 26px; 38 | position: relative; 39 | } 40 | .chat-response:before { 41 | display: block; 42 | content: ''; 43 | width: 24px; 44 | height: 24px; 45 | position: absolute; 46 | left: -26px; 47 | top: 6px; 48 | background-image: var(--peekobot-avatar); 49 | background-color: #FFF; 50 | background-repeat: no-repeat; 51 | background-size: 100%; 52 | border-radius: 100%; 53 | } 54 | .chat-ask { 55 | background-color: hsl(207, 96%, 55%); 56 | margin-right: 0; 57 | margin-left: auto; 58 | color: hsl(0, 0%, 100%); 59 | } 60 | .choices { 61 | opacity: 0; /* Set to active to show */ 62 | transform: translateY(-50%); /* Set to activated to move down */ 63 | transition: all 0.3s 0.3s; 64 | transition: opacity 0.3s 0.3s; 65 | margin-top: 0.5rem; 66 | margin-left: 22px; 67 | } 68 | .choice { 69 | display: inline-block; 70 | outline: none; 71 | border: 1px solid hsl(0, 0%, 0%); 72 | padding: 0.3rem 0.8rem; 73 | background-color: hsl(0, 0%, 100%); 74 | border-radius: 1rem; 75 | font-size: 0.9rem; 76 | line-height: 1.3; 77 | margin-bottom: 0.5rem; 78 | margin-right: 0.5rem; 79 | text-decoration: none; 80 | color: inherit; 81 | cursor: pointer; 82 | } 83 | .choice:disabled { 84 | color: hsl(0, 0%, 80%); 85 | border-color: hsl(0, 0%, 80%); 86 | } 87 | .activated { 88 | opacity: 1; 89 | transform: translateY(0); 90 | } 91 | .restart { 92 | position: absolute; 93 | bottom: 0.5rem; 94 | right: 0.5rem; 95 | outline: none; 96 | font-size: 12px; 97 | color: hsl(0, 0%, 50%); 98 | box-shadow: none; 99 | border: 1px solid hsl(0, 0%, 60%); 100 | border-radius: 1rem; 101 | background: hsl(0, 0%, 100%); 102 | padding: 0.2rem 0.5rem; 103 | cursor: pointer; 104 | } 105 | -------------------------------------------------------------------------------- /docs/peekobot.js: -------------------------------------------------------------------------------- 1 | const bot = function () { 2 | 3 | const peekobot = document.getElementById('peekobot'); 4 | const container = document.getElementById('peekobot-container'); 5 | const inner = document.getElementById('peekobot-inner'); 6 | let restartButton = null; 7 | 8 | const sleep = function (ms) { 9 | return new Promise(resolve => setTimeout(resolve, ms)); 10 | }; 11 | 12 | const scrollContainer = function () { 13 | inner.scrollTop = inner.scrollHeight; 14 | }; 15 | 16 | const insertNewChatItem = function (elem) { 17 | //container.insertBefore(elem, peekobot); 18 | peekobot.appendChild(elem); 19 | scrollContainer(); 20 | //debugger; 21 | elem.classList.add('activated'); 22 | }; 23 | 24 | const printResponse = async function (step) { 25 | const response = document.createElement('div'); 26 | response.classList.add('chat-response'); 27 | response.innerHTML = step.text; 28 | insertNewChatItem(response); 29 | 30 | await sleep(1500); 31 | 32 | if (step.options) { 33 | const choices = document.createElement('div'); 34 | choices.classList.add('choices'); 35 | step.options.forEach(function (option) { 36 | const button = document.createElement(option.url ? 'a' : 'button'); 37 | button.classList.add('choice'); 38 | button.innerHTML = option.text; 39 | if (option.url) { 40 | button.href = option.url; 41 | } else { 42 | button.dataset.next = option.next; 43 | } 44 | choices.appendChild(button); 45 | }); 46 | insertNewChatItem(choices); 47 | } else if (step.next) { 48 | printResponse(chat[step.next]); 49 | } 50 | }; 51 | 52 | const printChoice = function (choice) { 53 | const choiceElem = document.createElement('div'); 54 | choiceElem.classList.add('chat-ask'); 55 | choiceElem.innerHTML = choice.innerHTML; 56 | insertNewChatItem(choiceElem); 57 | }; 58 | 59 | const disableAllChoices = function () { 60 | const choices = document.querySelectorAll('.choice'); 61 | choices.forEach(function (choice) { 62 | choice.disabled = 'disabled'; 63 | }); 64 | return; 65 | }; 66 | 67 | const handleChoice = async function (e) { 68 | 69 | if (!e.target.classList.contains('choice') || 'A' === e.target.tagName) { 70 | // Target isn't a button, but could be a child of a button. 71 | var button = e.target.closest('#peekobot-container .choice'); 72 | 73 | if (button !== null) { 74 | button.click(); 75 | } 76 | 77 | return; 78 | } 79 | 80 | e.preventDefault(); 81 | const choice = e.target; 82 | 83 | disableAllChoices(); 84 | 85 | printChoice(choice); 86 | scrollContainer(); 87 | 88 | await sleep(1500); 89 | 90 | if (choice.dataset.next) { 91 | printResponse(chat[choice.dataset.next]); 92 | } 93 | // Need to disable buttons here to prevent multiple choices 94 | }; 95 | 96 | const handleRestart = function () { 97 | startConversation(); 98 | } 99 | 100 | const startConversation = function () { 101 | printResponse(chat[1]); 102 | } 103 | 104 | const init = function () { 105 | container.addEventListener('click', handleChoice); 106 | 107 | restartButton = document.createElement('button'); 108 | restartButton.innerText = "Restart"; 109 | restartButton.classList.add('restart'); 110 | restartButton.addEventListener('click', handleRestart); 111 | 112 | container.appendChild(restartButton); 113 | 114 | startConversation(); 115 | }; 116 | 117 | init(); 118 | } 119 | 120 | bot(); -------------------------------------------------------------------------------- /images/noun_chatbot_1585775.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /peekobot.css: -------------------------------------------------------------------------------- 1 | #peekobot-container { 2 | border: 1px solid hsl(0, 0%, 90%); 3 | border-radius: 12px; 4 | box-shadow: 2px 2px 5px hsl(0, 0%, 60%); 5 | padding: 1rem 1rem 2rem 1rem; 6 | height: var(--peekobot-height); 7 | position: relative; 8 | } 9 | #peekobot-inner { 10 | height: 100%; 11 | overflow-y: scroll; 12 | overflow-x: hidden; 13 | scroll-behavior: smooth; 14 | position: relative; 15 | /* Hiding scrollbars is a pain. This is from: https://redstapler.co/css-hidden-scrollbar-while-scrollable-element/ */ 16 | scrollbar-width: none; 17 | -ms-overflow-style: none; 18 | } 19 | #peekobot-inner::-webkit-scrollbar { 20 | display: none; /* Chrome Safari */ 21 | } 22 | 23 | .chat-response, 24 | .chat-ask { 25 | opacity: 0; /* Set to activated to show */ 26 | transform: translateY(-50%); /* Set to activated to move down */ 27 | transition: all 0.3s 0.3s; 28 | border-radius: 12px; 29 | background-color: hsl(0, 0%, 90%); 30 | padding: 0.5rem 0.7rem; 31 | line-height: 1.4; 32 | color: black; 33 | width: 80%; 34 | margin-bottom: 0.5rem; 35 | } 36 | .chat-response { 37 | margin-left: 26px; 38 | position: relative; 39 | } 40 | .chat-response:before { 41 | display: block; 42 | content: ''; 43 | width: 24px; 44 | height: 24px; 45 | position: absolute; 46 | left: -26px; 47 | top: 6px; 48 | background-image: var(--peekobot-avatar); 49 | background-color: #999; 50 | background-repeat: no-repeat; 51 | background-size: 100%; 52 | border-radius: 100%; 53 | } 54 | .chat-ask { 55 | background-color: hsl(207, 96%, 55%); 56 | margin-right: 0; 57 | margin-left: auto; 58 | color: hsl(0, 0%, 100%); 59 | } 60 | .choices { 61 | opacity: 0; /* Set to active to show */ 62 | transform: translateY(-50%); /* Set to activated to move down */ 63 | transition: all 0.3s 0.3s; 64 | transition: opacity 0.3s 0.3s; 65 | margin-top: 0.5rem; 66 | margin-left: 22px; 67 | } 68 | .choice { 69 | display: inline-block; 70 | outline: none; 71 | border: 1px solid hsl(0, 0%, 0%); 72 | padding: 0.3rem 0.8rem; 73 | background-color: hsl(0, 0%, 100%); 74 | border-radius: 1rem; 75 | font-size: 0.9rem; 76 | line-height: 1.3; 77 | margin-bottom: 0.5rem; 78 | margin-right: 0.5rem; 79 | text-decoration: none; 80 | color: inherit; 81 | cursor: pointer; 82 | } 83 | .choice:disabled { 84 | color: hsl(0, 0%, 80%); 85 | border-color: hsl(0, 0%, 80%); 86 | } 87 | .activated { 88 | opacity: 1; 89 | transform: translateY(0); 90 | } 91 | .restart { 92 | position: absolute; 93 | bottom: 0.5rem; 94 | right: 0.5rem; 95 | outline: none; 96 | font-size: 12px; 97 | color: hsl(0, 0%, 50%); 98 | box-shadow: none; 99 | border: 1px solid hsl(0, 0%, 60%); 100 | border-radius: 1rem; 101 | background: hsl(0, 0%, 100%); 102 | padding: 0.2rem 0.5rem; 103 | cursor: pointer; 104 | } 105 | -------------------------------------------------------------------------------- /peekobot.js: -------------------------------------------------------------------------------- 1 | const bot = function () { 2 | 3 | const peekobot = document.getElementById('peekobot'); 4 | const container = document.getElementById('peekobot-container'); 5 | const inner = document.getElementById('peekobot-inner'); 6 | let restartButton = null; 7 | 8 | const sleep = function (ms) { 9 | return new Promise(resolve => setTimeout(resolve, ms)); 10 | }; 11 | 12 | const scrollContainer = function () { 13 | inner.scrollTop = inner.scrollHeight; 14 | }; 15 | 16 | const insertNewChatItem = function (elem) { 17 | //container.insertBefore(elem, peekobot); 18 | peekobot.appendChild(elem); 19 | scrollContainer(); 20 | //debugger; 21 | elem.classList.add('activated'); 22 | }; 23 | 24 | const printResponse = async function (step) { 25 | const response = document.createElement('div'); 26 | response.classList.add('chat-response'); 27 | response.innerHTML = step.text; 28 | insertNewChatItem(response); 29 | 30 | await sleep(1500); 31 | 32 | if (step.options) { 33 | const choices = document.createElement('div'); 34 | choices.classList.add('choices'); 35 | step.options.forEach(function (option) { 36 | const button = document.createElement(option.url ? 'a' : 'button'); 37 | button.classList.add('choice'); 38 | button.innerHTML = option.text; 39 | if (option.url) { 40 | button.href = option.url; 41 | } else { 42 | button.dataset.next = option.next; 43 | } 44 | choices.appendChild(button); 45 | }); 46 | insertNewChatItem(choices); 47 | } else if (step.next) { 48 | printResponse(chat[step.next]); 49 | } 50 | }; 51 | 52 | const printChoice = function (choice) { 53 | const choiceElem = document.createElement('div'); 54 | choiceElem.classList.add('chat-ask'); 55 | choiceElem.innerHTML = choice.innerHTML; 56 | insertNewChatItem(choiceElem); 57 | }; 58 | 59 | const disableAllChoices = function () { 60 | const choices = document.querySelectorAll('.choice'); 61 | choices.forEach(function (choice) { 62 | choice.disabled = 'disabled'; 63 | }); 64 | return; 65 | }; 66 | 67 | const handleChoice = async function (e) { 68 | 69 | if (!e.target.classList.contains('choice') || 'A' === e.target.tagName) { 70 | // Target isn't a button, but could be a child of a button. 71 | var button = e.target.closest('#peekobot-container .choice'); 72 | 73 | if (button !== null) { 74 | button.click(); 75 | } 76 | 77 | return; 78 | } 79 | 80 | e.preventDefault(); 81 | const choice = e.target; 82 | 83 | disableAllChoices(); 84 | 85 | printChoice(choice); 86 | scrollContainer(); 87 | 88 | await sleep(1500); 89 | 90 | if (choice.dataset.next) { 91 | printResponse(chat[choice.dataset.next]); 92 | } 93 | // Need to disable buttons here to prevent multiple choices 94 | }; 95 | 96 | const handleRestart = function () { 97 | startConversation(); 98 | } 99 | 100 | const startConversation = function () { 101 | printResponse(chat[1]); 102 | } 103 | 104 | const init = function () { 105 | container.addEventListener('click', handleChoice); 106 | 107 | restartButton = document.createElement('button'); 108 | restartButton.innerText = "Restart"; 109 | restartButton.classList.add('restart'); 110 | restartButton.addEventListener('click', handleRestart); 111 | 112 | container.appendChild(restartButton); 113 | 114 | startConversation(); 115 | }; 116 | 117 | init(); 118 | } 119 | 120 | bot(); --------------------------------------------------------------------------------