king of ai chatbots

This sketch creates Larry, a conversational AI chatbot that runs entirely in your browser. Larry learns from your inputs, remembers conversations, detects mood and intent, and responds with personality using voice recognition and text-to-speech capabilities.

🧪 Try This!

Experiment with the code by making these changes:

  1. Teach Larry a fact and ask it back
  2. Activate personality modes
  3. Use the voice button
  4. Make the voice faster — Change the speech rate from 1.1 to 2.0 to make Larry speak twice as fast
  5. Disable voice responses — Comment out the speak() call so Larry types back to you instead of speaking
  6. Add a new joke — Add your own joke to the jokes array so Larry tells it when you ask for a joke
  7. Make Markov sentences longer — Increase the loop limit from 15 to 25 so generated sentences are longer and more complex
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch demonstrates a full-featured conversational AI named Larry that you interact with through text or voice. What makes it special is that Larry learns from conversations using Markov chains, remembers facts you teach it, detects your mood and conversational intent, and even speaks back using the Web Speech API. The code combines natural language processing techniques, localStorage persistence, and the p5.js DOM manipulation library to build a chatbot entirely in the browser.

The sketch is organized into several interconnected systems: a UI layer that manages the chat interface and buttons, a memory system that persists knowledge between sessions, and a brain that generates replies using intent detection, sentiment analysis, topic tracking, and Markov chain text generation. By studying it, you will learn how to parse user input with regular expressions, implement a simple knowledge base, use the Web Speech API for voice interaction, and structure a complex interactive application with p5.js.

⚙️ How It Works

  1. When the sketch loads, setup() creates the UI elements (title, chat history div, input field, send button, voice button) and loads any previously saved knowledge from localStorage using loadMemory().
  2. When you type a message and press Send (or Enter), sendCurrentMessage() adds your text to the chat, clears the input field, and shows a typing indicator.
  3. After a random delay (400–1200 ms), generateReply() processes your input to build a response: it pushes your text into memory arrays, learns word transitions using learnMarkov(), tracks topics, and detects your mood by searching for emotional keywords.
  4. generateReply() then runs a series of intent detection checks: does your message ask for a joke, the time, or contain a greeting? Does it try to teach Larry something new ("cats are fluffy") or ask what Larry knows? Does it contain math to solve? If none of these patterns match, Larry generates a sentence using the Markov brain (learned from all previous messages) or falls back to a personality-appropriate response.
  5. The bot's reply is added to the chat, spoken aloud using the Web Speech API's SpeechSynthesisUtterance, and the knowledge base is saved to localStorage so Larry remembers next time you visit.
  6. Voice input works through the Web Speech API: clicking the microphone button activates the browser's speech recognition, which listens for spoken words, converts them to text, fills the input field, and sends the message automatically.

🎓 Concepts You'll Learn

Text processing and regular expressionsMarkov chain text generationIntent detection and pattern matchingSentiment analysislocalStorage persistent memoryWeb Speech API (voice recognition and synthesis)DOM manipulation with p5.jsFuzzy string matching and similarity scoring

📝 Code Breakdown

setup()

setup() runs once when the page loads. It initializes the DOM structure (title, chat box, input field, buttons) and restores Larry's memory from localStorage so previous conversations are never forgotten.

function setup() {
  noCanvas();

  loadMemory();

  const app = select("#app");
  app.html("");

  createElement("h1", "Larry").parent(app);

  chatHistoryDiv = createDiv().parent(app);
  chatHistoryDiv.id("chatHistoryDiv");

  const row = createDiv().parent(app);
  row.addClass("row");

  inputField = createInput().parent(row);
  inputField.id("inputField");

  sendButton = createButton("Send");
  sendButton.parent(row);
  sendButton.id("sendButton");
  sendButton.mousePressed(sendCurrentMessage);

  voiceButton = createButton("🎤");
  voiceButton.parent(row);
  voiceButton.id("voiceButton");
  voiceButton.mousePressed(startVoice);

  inputField.elt.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
      e.preventDefault();
      sendCurrentMessage();
    }
  });

  addMessage("bot", "Hello. I am Larry 🤖");
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-calls DOM element creation createElement("h1", "Larry").parent(app);

Creates the title and attaches input field, chat history, and button elements to the page

function-call Load persisted memory loadMemory();

Restores Larry's knowledge base, topic counter, Markov brain, and user name from localStorage

event-listener Keyboard and button events inputField.elt.addEventListener("keydown", (e) => {

Allows sending messages by pressing Enter or clicking the Send button

noCanvas();
Tells p5.js not to create a default canvas—this sketch uses only HTML/DOM elements
loadMemory();
Retrieves Larry's saved knowledge base, topics, Markov chains, and user name from browser storage
const app = select("#app");
Selects the #app div from index.html so we can add chat UI elements to it
chatHistoryDiv = createDiv().parent(app);
Creates the scrollable chat history area where messages will appear
sendButton.mousePressed(sendCurrentMessage);
Connects the Send button to the sendCurrentMessage function so clicking it sends your message
voiceButton.mousePressed(startVoice);
Connects the microphone button to startVoice so you can activate speech recognition
if (e.key === "Enter") {
Detects when the user presses the Enter key in the input field
addMessage("bot", "Hello. I am Larry 🤖");
Displays Larry's greeting when the sketch first loads

sendCurrentMessage()

This function is the main pipeline: it takes your text, displays it, asks the brain for a reply, and shows the response with voice. The random delay makes conversation feel more natural—nobody responds instantly. Everything Larry learns is saved at the end.

🔬 These three lines happen instantly when you hit Send. What would happen if you removed the showTyping(true) line? Would you still know Larry is thinking?

  addMessage("user", text);
  inputField.value("");

  showTyping(true);
function sendCurrentMessage() {
  const text = inputField.value().trim();
  if (!text) return;

  addMessage("user", text);
  inputField.value("");

  showTyping(true);

  setTimeout(() => {
    const reply = generateReply(text);

    showTyping(false);

    addMessage("bot", reply);

    speak(reply);

    saveMemory();
  }, random(400, 1200));
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Empty input check if (!text) return;

Prevents sending blank messages

async-delay Simulated thinking time }, random(400, 1200));

Makes Larry seem like it's thinking before replying instead of responding instantly

const text = inputField.value().trim();
Gets the text you typed in the input field and removes leading/trailing whitespace
if (!text) return;
Stops if the message is empty—doesn't send or process anything
addMessage("user", text);
Displays your message in the chat as a blue bubble on the right side
inputField.value("");
Clears the input field so it's ready for your next message
showTyping(true);
Shows a 'Larry is thinking...' indicator so you know the bot is working
const reply = generateReply(text);
Calls the brain function that analyzes your input and creates a response
showTyping(false);
Removes the thinking indicator before displaying Larry's reply
addMessage("bot", reply);
Displays Larry's response as a green bubble on the left side
speak(reply);
Makes Larry speak the reply aloud using text-to-speech
saveMemory();
Saves the conversation to localStorage so Larry remembers it next time
}, random(400, 1200));
The entire reply process happens after a random delay between 400 and 1200 milliseconds to simulate thinking time

generateReply(input)

generateReply() is the heart of Larry's AI: it runs through a decision tree of pattern matches, regexes, and special cases. At each level it checks a different category (intent, name, facts, math, memory, personality modes). If nothing matches, it falls back to Markov-generated text or a random personality response. The order matters—intent checks happen first so greetings are answered before anything else.

🔬 This block checks intent: if you say 'hello' it returns 'Hello!' instantly. What happens if you add another intent check, like 'if (intent === "weather") return "It's sunny!";'? When would Larry use that response?

  //----------------
  // INTENT
  //----------------
  let intent = detectIntent(text);

  if (intent === "greet") return "Hello!";
  if (intent === "bye") return "Goodbye.";
  if (intent === "thanks") return "You're welcome.";

🔬 This is how Larry learns facts: you say 'cats are fluffy' and it stores 'cats' → 'fluffy'. What if you changed the pattern to require more structure, like 'teach me that (.+) are (.+)'? Would Larry learn more or less often?

  let teach = text.match(/(.+?) are (.+)/);

  if (teach) {
    knowledgeBase[teach[1]] = teach[2];

    return `I learned that ${teach[1]} are ${teach[2]}.`;
  }
function generateReply(input) {
  const text = input.toLowerCase();

  conversationMemory.push(input);
  contextWindow.push(input);

  if (contextWindow.length > 5) contextWindow.shift();

  sentenceMemory.push(input);

  learnMarkov(input);

  trackTopics(text);

  detectMood(text);

  //----------------
  // INTENT
  //----------------
  let intent = detectIntent(text);

  if (intent === "greet") return "Hello!";
  if (intent === "bye") return "Goodbye.";
  if (intent === "thanks") return "You're welcome.";
  if (intent === "joke") return randomChoice(jokes);
  if (intent === "time") return new Date().toLocaleTimeString();

  //----------------
  // NAME MEMORY
  //----------------
  let name = text.match(/my name is (\w+)/);

  if (name) {
    userName = name[1];
    return `Nice to meet you ${userName}!`;
  }

  if (text.includes("what is my name") && userName)
    return `Your name is ${userName}.`;

  //----------------
  // TEACHING
  //----------------
  let teach = text.match(/(.+?) are (.+)/);

  if (teach) {
    knowledgeBase[teach[1]] = teach[2];

    return `I learned that ${teach[1]} are ${teach[2]}.`;
  }

  //----------------
  // ASK KNOWLEDGE
  //----------------
  let ask = text.match(/what are (.+)/);

  if (ask) {
    let key = findClosestKnowledge(ask[1]);

    if (key) return `${key} are ${knowledgeBase[key]}.`;

    return "I don't know that yet.";
  }

  //----------------
  // MATH
  //----------------
  let math = text.match(/(-?\d+(\.\d+)?\s*[\+\-\*\/]\s*-?\d+(\.\d+)?)/);

  if (math) {
    try {
      let expr = math[1];
      let val = Function('"use strict";return(' + expr + ')')();
      return `Result: ${val}`;
    } catch {}
  }

  //----------------
  // MEMORY RECALL
  //----------------
  if (text.includes("what did i say")) {
    let last = conversationMemory.slice(-5, -1);

    return "You said: " + last.join(", ");
  }

  //----------------
  // SELF KNOWLEDGE
  //----------------
  if (text.includes("what do you know")) {
    let keys = Object.keys(knowledgeBase);

    if (keys.length === 0) return "I don't know much yet.";

    return "I know about " + keys.join(", ");
  }

  //----------------
  // PERSONALITY MODE
  //----------------
  if (text.includes("be philosophical")) {
    personalityMode = "philosopher";
    return "Entering philosopher mode.";
  }

  if (text.includes("be robotic")) {
    personalityMode = "robot";
    return "Switching to robotic personality.";
  }

  //----------------
  // MARKOV AI
  //----------------
  let sentence = generateMarkovSentence();

  if (sentence) return sentence;

  //----------------
  // TOPIC RESPONSE
  //----------------
  if (currentTopic) {
    return `Let's talk more about ${currentTopic}.`;
  }

  //----------------
  // FALLBACK
  //----------------
  return randomChoice(personalities[personalityMode]);
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

calculation Memory storage conversationMemory.push(input);

Stores your message in three different memory arrays for different purposes

conditional Intent detection if (intent === "greet") return "Hello!";

Checks if you said something like 'hello' and responds with a greeting

regex-match Name extraction let name = text.match(/my name is (\w+)/);

Looks for the pattern 'my name is [word]' and extracts your name

regex-match Knowledge teaching let teach = text.match(/(.+?) are (.+)/);

Captures sentences like 'cats are fluffy' and stores the fact in the knowledge base

regex-match Math expression let math = text.match(/(-?\d+(\.\d+)?\s*[\+\-\*\/]\s*-?\d+(\.\d+)?)/);

Finds math expressions like '5 + 3' and calculates the result

function-call Markov sentence generation let sentence = generateMarkovSentence();

If no pattern matched, generate a sentence from learned word transitions

const text = input.toLowerCase();
Converts your message to lowercase so pattern matching works even if you used capitals
conversationMemory.push(input);
Saves your message to the long-term conversation history array
contextWindow.push(input);
Saves your message to a short-term 5-message window for recent context
if (contextWindow.length > 5) contextWindow.shift();
Keeps the context window to only the last 5 messages by removing old ones
learnMarkov(input);
Analyzes word pairs in your message and learns them for future sentence generation
trackTopics(text);
Counts which long words you used most to detect what topics you care about
detectMood(text);
Scans for emotional words to determine if you seem happy or sad
let intent = detectIntent(text);
Checks what type of message this is: greeting, goodbye, joke request, or unknown
if (intent === "greet") return "Hello!";
If you said hello/hi/hey, return a greeting instead of analyzing further
let name = text.match(/my name is (\w+)/);
Uses a regex pattern to extract your name from sentences like 'my name is Alice'
if (name) {
If a name pattern was found, remember it and confirm
let teach = text.match(/(.+?) are (.+)/);
Looks for the pattern '[subject] are [description]' like 'dogs are loyal'
knowledgeBase[teach[1]] = teach[2];
Stores the fact in the knowledge base: the subject becomes the key, the description becomes the value
let ask = text.match(/what are (.+)/);
Looks for questions starting with 'what are' to trigger knowledge recall
let key = findClosestKnowledge(ask[1]);
Uses fuzzy matching to find the closest fact to what you asked about
let math = text.match(/(-?\d+(\.\d+)?\s*[\+\-\*\/]\s*-?\d+(\.\d+)?)/);
Uses a regex to find math expressions like '5 + 3' or '-10 * 2.5'
let val = Function('"use strict";return(' + expr + ')')();
Safely evaluates the math expression by creating a temporary function and running it
if (text.includes("what did i say")) {
If you ask 'what did i say', retrieve your last 5 messages from memory
let sentence = generateMarkovSentence();
If no special pattern matched, generate a sentence using the Markov chain brain
return randomChoice(personalities[personalityMode]);
If nothing else works, pick a random safe response in Larry's current personality mode

detectIntent(text)

detectIntent() is a simple keyword-matching system: it looks for specific trigger words and returns a category label. This is efficient and easy to understand, but very literal—it won't understand synonyms or context. For a more advanced chatbot, you could use AI libraries like ml5.js or TensorFlow.js to classify intents by meaning instead of keywords.

function detectIntent(text) {
  const intents = [
    { type: "greet", words: ["hello", "hi", "hey"] },
    { type: "bye", words: ["bye", "goodbye"] },
    { type: "thanks", words: ["thanks", "thank you"] },
    { type: "joke", words: ["joke", "funny"] },
    { type: "time", words: ["time"] }
  ];

  for (let i of intents) {
    for (let w of i.words) {
      if (text.includes(w)) return i.type;
    }
  }

  return "unknown";
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Intent matching loop for (let i of intents) {

Iterates through each intent category to find a match

const intents = [
Creates an array of intent objects, each with a type and list of trigger words
{ type: "greet", words: ["hello", "hi", "hey"] },
If the user says any of these words, the intent is 'greet'
for (let i of intents) {
Loops through each intent object one at a time
for (let w of i.words) {
Loops through the trigger words for the current intent
if (text.includes(w)) return i.type;
If the user's text contains this word, return the intent type immediately and stop searching
return "unknown";
If no intent matched, return 'unknown' so generateReply() knows to try other methods

detectMood(text)

detectMood() scans your message for emotional keywords and updates a mood variable. Currently, this mood isn't used in replies—but you could modify generateReply() to respond differently based on userMood. For example, if mood is 'sad', Larry could offer encouragement instead of jokes.

function detectMood(text) {
  const positive = ["happy", "great", "awesome", "good"];
  const negative = ["sad", "bad", "angry", "upset"];

  positive.forEach((w) => {
    if (text.includes(w)) userMood = "happy";
  });

  negative.forEach((w) => {
    if (text.includes(w)) userMood = "sad";
  });
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

forEach-loop Positive word detection positive.forEach((w) => {

Checks if the user said any happy words

forEach-loop Negative word detection negative.forEach((w) => {

Checks if the user said any sad or angry words

const positive = ["happy", "great", "awesome", "good"];
Array of words that indicate the user is in a positive mood
const negative = ["sad", "bad", "angry", "upset"];
Array of words that indicate the user is in a negative mood
positive.forEach((w) => {
For each positive word, check if it's in the user's message
if (text.includes(w)) userMood = "happy";
If a positive word was found, set the global userMood variable to 'happy'
negative.forEach((w) => {
For each negative word, check if it's in the user's message
if (text.includes(w)) userMood = "sad";
If a negative word was found, set the global userMood variable to 'sad'

trackTopics(text)

trackTopics() keeps track of what you talk about most. It counts words longer than 4 characters to filter out noise words. The currentTopic variable is used in generateReply() as a fallback: if nothing else works, Larry suggests talking more about the most recent topic you mentioned.

function trackTopics(text) {
  let words = text.split(" ");

  words.forEach((w) => {
    if (w.length > 4) {
      topicCounter[w] = (topicCounter[w] || 0) + 1;

      currentTopic = w;
    }
  });
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Text tokenization let words = text.split(" ");

Breaks the user's message into individual words

forEach-loop Topic frequency tracking words.forEach((w) => {

Counts how many times each word (longer than 4 characters) has been mentioned

let words = text.split(" ");
Splits the user's message into an array of words using spaces as dividers
words.forEach((w) => {
Loops through each word one at a time
if (w.length > 4) {
Only tracks words longer than 4 characters to ignore common small words like 'the', 'is', 'and'
topicCounter[w] = (topicCounter[w] || 0) + 1;
Increments a count for this word: if it doesn't exist yet, start at 0; otherwise add 1
currentTopic = w;
Updates the global currentTopic to the most recent long word (used in fallback responses)

learnMarkov(sentence)

learnMarkov() builds a Markov chain by storing which words naturally follow each other. A Markov chain is a statistical model where each word's future depends only on the current word, not on history. This is the core of Larry's sentence generation—by learning from all your inputs, Larry can create novel sentences that sound like you.

🔬 This loop learns word pairs: 'hello world' teaches that 'hello' can be followed by 'world'. What if you changed it to look TWO words ahead instead of one? How would that change the generated sentences?

  for (let i = 0; i < words.length - 1; i++) {
    let w = words[i].toLowerCase();
    let next = words[i + 1].toLowerCase();

    if (!markovBrain[w]) markovBrain[w] = [];

    markovBrain[w].push(next);
  }
function learnMarkov(sentence) {
  let words = sentence.split(" ");

  for (let i = 0; i < words.length - 1; i++) {
    let w = words[i].toLowerCase();
    let next = words[i + 1].toLowerCase();

    if (!markovBrain[w]) markovBrain[w] = [];

    markovBrain[w].push(next);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Word pair extraction for (let i = 0; i < words.length - 1; i++) {

Loops through consecutive words to capture word transitions

let words = sentence.split(" ");
Breaks the sentence into words
for (let i = 0; i < words.length - 1; i++) {
Loops through each word except the last one (because each word needs a 'next' word)
let w = words[i].toLowerCase();
Gets the current word and converts it to lowercase for consistency
let next = words[i + 1].toLowerCase();
Gets the next word after it, also in lowercase
if (!markovBrain[w]) markovBrain[w] = [];
If this word hasn't been learned before, create an empty array to hold its possible next words
markovBrain[w].push(next);
Adds the next word to the list of words that can follow this word

generateMarkovSentence()

generateMarkovSentence() is where Larry creates original text. It picks a random starting word and then follows the chain of learned word transitions, building a sentence step by step. The longer the loop, the longer and wordier Larry becomes. Early in a conversation when Larry hasn't learned much, this function returns null and generateReply() falls back to other methods.

🔬 The loop starts with a random word and then chains 15 more words. What happens if you change it to always start with the same word, like the most common word? Would sentences be more or less varied?

  let word = randomChoice(keys);

  let result = [word];

  for (let i = 0; i < 15; i++) {
function generateMarkovSentence() {
  let keys = Object.keys(markovBrain);

  if (keys.length < 5) return null;

  let word = randomChoice(keys);

  let result = [word];

  for (let i = 0; i < 15; i++) {
    let next = markovBrain[word];

    if (!next) break;

    word = randomChoice(next);

    result.push(word);
  }

  return result.join(" ");
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Training threshold if (keys.length < 5) return null;

Only generates sentences if Larry has learned at least 5 different words

for-loop Sentence building for (let i = 0; i < 15; i++) {

Builds up to 15 words by repeatedly picking the next word based on probabilities

let keys = Object.keys(markovBrain);
Gets an array of all the words that Larry has learned (the keys of the markovBrain object)
if (keys.length < 5) return null;
If Larry hasn't learned enough words yet, don't try to generate a sentence—return null
let word = randomChoice(keys);
Picks a random word from the learned words as a starting point
let result = [word];
Creates an array with the starting word
for (let i = 0; i < 15; i++) {
Loops up to 15 times to add words to the sentence
let next = markovBrain[word];
Looks up the current word in the markovBrain to get the list of words that can follow it
if (!next) break;
If the current word has never been followed by anything, stop generating
word = randomChoice(next);
Randomly picks one of the possible next words from the list
result.push(word);
Adds the new word to the sentence
return result.join(" ");
Joins all the words with spaces and returns the final sentence

findClosestKnowledge(word)

findClosestKnowledge() uses fuzzy matching to find the best fact in Larry's knowledge base. The threshold of 0.4 (40% similarity) prevents Larry from returning wrong facts when you ask about something it's never learned. You could lower this threshold to make Larry guess more often, or raise it to make Larry more conservative.

function findClosestKnowledge(word) {
  let best = null;
  let bestScore = 0;

  for (let key in knowledgeBase) {
    let score = similarity(word, key);

    if (score > bestScore) {
      bestScore = score;
      best = key;
    }
  }

  if (bestScore > 0.4) return best;

  return null;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Knowledge search loop for (let key in knowledgeBase) {

Compares the user's query against all stored knowledge to find the closest match

let best = null;
Initializes a variable to store the best matching key found so far
let bestScore = 0;
Initializes a score tracker starting at 0
for (let key in knowledgeBase) {
Loops through every stored fact in the knowledge base
let score = similarity(word, key);
Calculates how similar the user's word is to this knowledge base key (0 = different, 1 = identical)
if (score > bestScore) {
If this is the best match found so far, update the best match variables
if (bestScore > 0.4) return best;
Only return a match if it scored above 0.4 (40% similar)—otherwise return null to indicate no match

similarity(a, b)

similarity() is a simple string comparison function that measures how many characters match at the beginning of two words. For example, 'cat' and 'car' would score 2/3 = 0.67 (two matching characters out of three). This is primitive but effective for a quick fuzzy match—real-world NLP uses more advanced algorithms like Levenshtein distance.

function similarity(a, b) {
  let same = 0;

  for (let i = 0; i < Math.min(a.length, b.length); i++) {
    if (a[i] === b[i]) same++;
  }

  return same / Math.max(a.length, b.length);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Character matching for (let i = 0; i < Math.min(a.length, b.length); i++) {

Compares the user's word character-by-character against a knowledge base key

let same = 0;
Counter for matching characters
for (let i = 0; i < Math.min(a.length, b.length); i++) {
Loops through characters up to the length of the shorter string
if (a[i] === b[i]) same++;
If the characters match, increment the counter
return same / Math.max(a.length, b.length);
Divides matching characters by the length of the longer word to get a similarity score (0 to 1)

startVoice()

startVoice() activates the Web Speech API's speech recognition system. When you click the microphone button, the browser asks for microphone permission (the first time), then listens until it detects the end of speech. The recognized text is automatically sent as a message. This is one of the coolest features—it feels like talking to Larry directly.

function startVoice() {
  if (!("webkitSpeechRecognition" in window)) {
    alert("Speech recognition not supported in this browser.");
    return;
  }

  recognition = new webkitSpeechRecognition();
  recognition.lang = 'en-US';
  recognition.interimResults = false;
  recognition.maxAlternatives = 1;

  recognition.onresult = (e) => {
    let speech = e.results[0][0].transcript;

    inputField.value(speech);

    sendCurrentMessage();
  };

  recognition.onerror = (e) => {
    console.error("Speech recognition error:", e.error);
    alert("Speech recognition error: " + e.error);
  };

  recognition.onend = () => {
    console.log("Speech recognition ended.");
  };

  recognition.start();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Browser capability check if (!("webkitSpeechRecognition" in window)) {

Checks if the browser supports the Web Speech API

event-handler Speech result handler recognition.onresult = (e) => {

Triggered when the browser successfully recognizes spoken words

if (!("webkitSpeechRecognition" in window)) {
Checks if the browser has the webkitSpeechRecognition API (most browsers use the webkit prefix)
alert("Speech recognition not supported in this browser.");
If not supported, shows an error message and stops
recognition = new webkitSpeechRecognition();
Creates a new speech recognition object that listens to the user's microphone
recognition.lang = 'en-US';
Sets the language for recognition to American English
recognition.interimResults = false;
Only capture final results, not partial speech as the user is still talking
recognition.maxAlternatives = 1;
Only get the single most likely transcription, not multiple guesses
recognition.onresult = (e) => {
This function runs when the browser finishes listening and recognizes words
let speech = e.results[0][0].transcript;
Extracts the transcribed text from the speech recognition results
inputField.value(speech);
Puts the recognized text into the input field so you can see what was heard
sendCurrentMessage();
Automatically sends the message without requiring you to click Send
recognition.onerror = (e) => {
This function runs if there's an error during speech recognition
recognition.start();
Activates the microphone and begins listening for speech

speak(text)

speak() uses the Web Speech API's SpeechSynthesis interface to convert text to audio. The rate and pitch properties let you customize the voice—higher pitch sounds younger or more feminine, lower sounds deeper. Different browsers support different voices; you can explore speechSynthesis.getVoices() for advanced voice selection.

function speak(text) {
  let utter = new SpeechSynthesisUtterance(text);
  utter.rate = 1.1;
  utter.pitch = 1.0;

  speechSynthesis.speak(utter);
}
Line-by-line explanation (4 lines)
let utter = new SpeechSynthesisUtterance(text);
Creates a speech utterance object from the text—this is what the browser will speak aloud
utter.rate = 1.1;
Sets the speech speed to 1.1x normal speed—a bit faster than natural conversation
utter.pitch = 1.0;
Sets the pitch to 1.0 (normal pitch)—could be higher for a squeaky voice or lower for a deep voice
speechSynthesis.speak(utter);
Tells the browser to play the speech immediately through the speaker

addMessage(sender, text)

addMessage() handles all the visual display of chat messages. It creates a div, applies the right CSS class for styling, and automatically scrolls the chat to the bottom. The escapeHtml() call prevents JavaScript injection if someone tries to send malicious HTML in their message.

function addMessage(sender, text) {
  let msg = createDiv();

  msg.addClass(sender === "user" ? "user-message" : "bot-message");

  let name = sender === "user" ? "You" : "Larry";

  msg.html(`<b>${name}:</b> ${escapeHtml(text)}`);

  msg.parent(chatHistoryDiv);

  chatHistoryDiv.elt.scrollTop = chatHistoryDiv.elt.scrollHeight;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Message styling msg.addClass(sender === "user" ? "user-message" : "bot-message");

Applies different CSS classes depending on whether the message is from you or Larry

let msg = createDiv();
Creates a new div element to hold this message
msg.addClass(sender === "user" ? "user-message" : "bot-message");
Adds the CSS class 'user-message' (blue bubble) if you sent it, or 'bot-message' (green bubble) if Larry did
let name = sender === "user" ? "You" : "Larry";
Sets the label—either 'You:' or 'Larry:' depending on who sent the message
msg.html(`<b>${name}:</b> ${escapeHtml(text)}`);
Fills the div with bold name label and the message text, safely escaped to prevent HTML injection
msg.parent(chatHistoryDiv);
Adds this message div to the chat history div so it displays on screen
chatHistoryDiv.elt.scrollTop = chatHistoryDiv.elt.scrollHeight;
Auto-scrolls to the bottom of the chat so you always see the newest message

showTyping(on)

showTyping() creates and removes a visual indicator while Larry is thinking. It's a simple way to give the user feedback that something is happening. The message appears after sendCurrentMessage() and disappears before the reply is added.

function showTyping(on) {
  if (on) {
    typingIndicator = createDiv("Larry is thinking...");
    typingIndicator.id("typingIndicator");
    typingIndicator.parent(chatHistoryDiv);
    chatHistoryDiv.elt.scrollTop = chatHistoryDiv.elt.scrollHeight;
  } else {
    if (typingIndicator) {
      typingIndicator.remove();
      typingIndicator = null;
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Typing display toggle if (on) {

Shows or hides the typing indicator based on the on parameter

if (on) {
If on is true, show the typing indicator
typingIndicator = createDiv("Larry is thinking...");
Creates a new div with the text 'Larry is thinking...'
typingIndicator.parent(chatHistoryDiv);
Adds it to the chat history so it displays
} else {
If on is false, hide the typing indicator
typingIndicator.remove();
Deletes the typing indicator div from the page

saveMemory()

saveMemory() persists Larry's learning to localStorage, a built-in browser database that survives page reloads and even browser restarts. When you reload the page, loadMemory() retrieves this data so Larry remembers you and everything you've taught it. This is what makes Larry special—it's not a stateless bot, it has actual persistence.

function saveMemory() {
  localStorage.setItem(
    "larryBrain",
    JSON.stringify({
      knowledgeBase,
      topicCounter,
      markovBrain,
      userName
    })
  );
}
Line-by-line explanation (7 lines)
localStorage.setItem(
Saves data to the browser's local storage (persistent across page reloads)
"larryBrain",
The key name under which all of Larry's data is stored
JSON.stringify({
Converts the JavaScript object into a JSON string so it can be stored as text
knowledgeBase,
Saves all the facts Larry has learned (like 'dogs' → 'loyal')
topicCounter,
Saves which words and topics you've mentioned most
markovBrain,
Saves all the word transition chains Larry has learned
userName
Saves your name if you told Larry what it is

loadMemory()

loadMemory() is the counterpart to saveMemory()—it retrieves everything Larry learned from previous sessions. The `|| {}` and `|| null` patterns are fallbacks: if a piece of data doesn't exist, use an empty object or null instead of crashing.

function loadMemory() {
  let data = localStorage.getItem("larryBrain");

  if (data) {
    let d = JSON.parse(data);

    knowledgeBase = d.knowledgeBase || {};
    topicCounter = d.topicCounter || {};
    markovBrain = d.markovBrain || {};
    userName = d.userName || null;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Memory loading check if (data) {

Only loads data if it exists in localStorage

let data = localStorage.getItem("larryBrain");
Retrieves the stored JSON string from localStorage, or null if nothing was saved yet
if (data) {
Only proceeds if there's data to load
let d = JSON.parse(data);
Converts the JSON string back into a JavaScript object
knowledgeBase = d.knowledgeBase || {};
Restores the knowledge base, or starts with an empty object if nothing was saved
topicCounter = d.topicCounter || {};
Restores topic counts, or starts empty
markovBrain = d.markovBrain || {};
Restores the Markov chains, or starts empty
userName = d.userName || null;
Restores your saved name, or sets to null if you never told Larry your name

randomChoice(arr)

randomChoice() is a utility function used throughout the code to pick random items from arrays (jokes, personalities, words, etc.). The Math.floor() rounds down the random decimal to a valid array index.

function randomChoice(arr) {
  if (!Array.isArray(arr) || arr.length === 0) return null;
  return arr[Math.floor(Math.random() * arr.length)];
}
Line-by-line explanation (2 lines)
if (!Array.isArray(arr) || arr.length === 0) return null;
Safety check: if the input isn't an array or is empty, return null instead of crashing
return arr[Math.floor(Math.random() * arr.length)];
Generates a random number between 0 and the array length minus 1, then returns that element

escapeHtml(str)

escapeHtml() is a security function that prevents HTML/JavaScript injection attacks. If someone tried to send a message with `<script>` tags, escapeHtml() would convert them to harmless text so the browser displays them instead of executing them.

function escapeHtml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}
Line-by-line explanation (3 lines)
.replace(/&/g, "&amp;")
Replaces all ampersands with &amp; to prevent them being interpreted as HTML
.replace(/</g, "&lt;")
Replaces all < symbols with &lt; so they can't start HTML tags
.replace(/>/g, "&gt;")
Replaces all > symbols with &gt; to close the escape sequence

📦 Key Variables

chatHistoryDiv p5.Renderer

Stores the scrollable div where all chat messages are displayed

let chatHistoryDiv;
inputField p5.Renderer

Stores the text input element where you type your messages

let inputField;
sendButton p5.Renderer

Stores the Send button element

let sendButton;
voiceButton p5.Renderer

Stores the microphone button for voice input

let voiceButton;
conversationMemory array

Stores the entire history of messages from the user for long-term recall

let conversationMemory = [];
sentenceMemory array

Stores all user sentences for Markov learning

let sentenceMemory = [];
knowledgeBase object

Stores facts that Larry learns from you (keys = subjects, values = descriptions)

let knowledgeBase = {};
topicCounter object

Counts how many times each word topic has been mentioned

let topicCounter = {};
userName string

Stores the user's name if they told Larry what it is

let userName = null;
markovBrain object

Stores word transition chains for Markov sentence generation (keys = words, values = arrays of possible next words)

let markovBrain = {};
contextWindow array

Stores the most recent 5 messages for short-term context

let contextWindow = [];
userMood string

Tracks whether the user seems happy, sad, or neutral based on their word choices

let userMood = "neutral";
currentTopic string

Stores the most recently mentioned topic to use as fallback conversation

let currentTopic = null;
personalityMode string

Determines which personality responses Larry uses ('friendly', 'philosopher', 'robot', or 'curious')

let personalityMode = "friendly";
recognition webkitSpeechRecognition

Stores the Web Speech API object for voice recognition

let recognition;
typingIndicator p5.Renderer

Stores the 'Larry is thinking...' message div while a response is being generated

let typingIndicator;
personalities object

Object storing arrays of fallback responses for different personality modes

const personalities = { friendly: [...], philosopher: [...], robot: [...], curious: [...] };
jokes array

Array of jokes that Larry tells when you ask for one

const jokes = ["Why do programmers...", ...];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG generateReply()

The regex for teaching (/.+? are .+/) is too broad and will match conversational sentences like 'I am happy' and try to store them as facts, polluting the knowledge base

💡 Make the regex more specific: /teach me that (.+?) are (.+)/ or /(.+?) are (.+) right\?/ to require explicit teaching intent

BUG startVoice()

If speech recognition fails, the error alert may not explain what went wrong, and the recognition object isn't cleaned up for retry

💡 Add recognition.abort() in the onerror handler and provide more specific error messages like 'Microphone permission denied' or 'No speech detected'

PERFORMANCE learnMarkov()

Every single message splits words and iterates through all pairs, but if a user sends the same message twice, all pairs are duplicated in markovBrain—this wastes memory

💡 Add a check to avoid duplicate word pairs: if (!markovBrain[w].includes(next)) before pushing

STYLE generateReply()

The function is very long (150+ lines) with many nested conditionals, making it hard to follow the logic flow and add new features

💡 Refactor into smaller functions like checkTeaching(), checkMath(), checkMemory() to separate concerns and improve readability

FEATURE generateReply()

When users teach facts, there's no way to forget or update knowledge—if you say 'cats are fluffy' then later 'cats are mean', Larry keeps both and the second overwrites the first with no warning

💡 Add a 'forget [fact]' command to allow users to clear stored knowledge, and warn when overwriting existing facts

FEATURE speak()

All browsers use the same default voice—there's no way to pick a different voice from the system's available options

💡 Add a voice selector dropdown: const voices = speechSynthesis.getVoices(); utter.voice = voices[selectedVoiceIndex];

🔄 Code Flow

Code flow showing setup, sendcurrentmessage, generatereply, detectintent, detectmood, tracktopics, learnmarkov, generatemarkovsentence, findclosestknowledge, similarity, startvice, speak, addmessage, showtyping, savememory, loadmemory, randomchoice, escapehtml

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> ui-creation[ui-creation] draw --> memory-load[memory-load] draw --> event-listeners[event-listeners] click setup href "#fn-setup" click draw href "#fn-draw" click ui-creation href "#sub-ui-creation" click memory-load href "#sub-memory-load" click event-listeners href "#sub-event-listeners" memory-load --> loadmemory[loadMemory] loadmemory --> data-retrieval[data-retrieval] data-retrieval -->|exists| memory-push[memory-push] data-retrieval -->|not exists| endload[End Load] click loadmemory href "#fn-loadmemory" click memory-push href "#sub-memory-push" click data-retrieval href "#sub-data-retrieval" event-listeners --> input-validation[input-validation] input-validation -->|valid| sendcurrentmessage[sendCurrentMessage] input-validation -->|invalid| endinput[End Input] click input-validation href "#sub-input-validation" sendcurrentmessage --> typing-delay[typing-delay] typing-delay --> showtyping[showTyping] showtyping --> generatereply[generateReply] click sendcurrentmessage href "#fn-sendcurrentmessage" click typing-delay href "#sub-typing-delay" click showtyping href "#sub-show-typing" generatereply --> intent-check[intent-check] intent-check --> intent-loop[intent-loop] intent-loop --> word-search[word-search] word-search -->|found| response[Response] word-search -->|not found| markov-fallback[markov-fallback] click generatereply href "#fn-generatereply" click intent-check href "#sub-intent-check" click intent-loop href "#sub-intent-loop" click word-search href "#sub-word-search" click markov-fallback href "#sub-markov-fallback" markov-fallback --> learnmarkov[learnMarkov] learnmarkov -->|learned| generatemarkovsentence[generateMarkovSentence] generatemarkovsentence --> endresponse[End Response] click learnmarkov href "#fn-learnmarkov" click generatemarkovsentence href "#fn-generatemarkovsentence" response --> addmessage[addMessage] addmessage --> savememory[saveMemory] savememory --> endmessage[End Message] click addmessage href "#fn-addmessage" click savememory href "#fn-savememory" showtyping -->|show| show-check[show-check] show-check --> endtyping[End Typing] click show-check href "#sub-show-check" endload --> endmessage endinput --> endmessage endresponse --> endmessage endtyping --> endmessage endmessage --> draw

❓ Frequently Asked Questions

What visual elements does the 'king of ai chatbots' sketch include?

The sketch features a simple, clean user interface with a chat history display, input field, and buttons for sending messages and using voice input.

How can users engage with Larry the chatbot in this sketch?

Users can type their messages directly into the input field or use the microphone button to speak, allowing for an interactive conversation with Larry.

What creative coding concepts are showcased in this p5.js sketch?

This sketch demonstrates techniques such as real-time chat interaction, memory management for conversation context, and personality-driven responses using predefined personality modes and jokes.

Preview

king of ai chatbots - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of king of ai chatbots - Code flow showing setup, sendcurrentmessage, generatereply, detectintent, detectmood, tracktopics, learnmarkov, generatemarkovsentence, findclosestknowledge, similarity, startvice, speak, addmessage, showtyping, savememory, loadmemory, randomchoice, escapehtml
Code Flow Diagram