king of ai chatbots v2

This sketch creates Larry, an interactive AI chatbot built entirely in p5.js that learns from conversations, remembers user information, and adapts its personality. Users can type or speak to chat with Larry, who responds intelligently using Markov chains, intent detection, and persistent memory stored in the browser.

🧪 Try This!

Experiment with the code by making these changes:

  1. Teach Larry a fact
  2. Change Larry's personality
  3. Ask Larry about you
  4. Make Larry speak faster — Change the speech rate to make Larry's voice zip through replies more quickly
  5. Add more greetings — Expand the greeting intent list so Larry recognizes more ways of saying hello
  6. Change the typing indicator message — Customize what appears while Larry is thinking to match a different personality
  7. Shorten generated sentences — Reduce the loop count so Markov-generated sentences are shorter and punchier
  8. Add a custom joke — Add a new joke to the jokes array so Larry has something fresh to say when you ask for a joke
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch implements Larry, a full-featured conversational AI that runs directly in your browser using p5.js. Larry combines multiple AI techniques: intent detection (recognizing greetings and questions), Markov chain text generation (learning word patterns from what you say), persistent memory (storing what it learns and who you are), mood detection, and even text-to-speech. The interface is a chat window where you type or speak, and Larry responds with generated text that it also speaks aloud.

The code is organized into distinct systems: a brain layer that generates replies using multiple strategies (intent matching, knowledge recall, Markov generation), a voice layer handling speech recognition and synthesis, a UI layer that renders the chat interface, and a storage layer that persists Larry's memory across browser sessions. By studying this sketch you will learn how to combine multiple AI techniques, manage complex state in a chatbot, filter unsafe outputs, and integrate voice APIs into p5.js applications.

⚙️ How It Works

  1. When the sketch loads, setup() creates an HTML chat interface (title, message history div, input field, Send button, and voice button) and loads any previously saved memory from localStorage so Larry remembers past conversations.
  2. When you type a message and press Send or Enter, sendCurrentMessage() adds your text to the chat, clears the input, shows a typing indicator, and calls generateReply() after a random delay to simulate thinking time.
  3. generateReply() processes your input through several AI layers: it detects your intent (hello, goodbye, joke request, time request), it learns new word patterns via Markov chains, it tracks topics you mention, and it updates your detected mood (happy, sad, neutral).
  4. If a specific intent is recognized, Larry responds with a hardcoded reply; otherwise it checks memory for learned facts about you (your name, things you taught it), looks for math expressions to calculate, or recalls what you said earlier.
  5. If none of those strategies produce a reply, Larry generates a new sentence by randomly walking through the Markov chain (picking a starting word, then repeatedly picking the next word that most commonly followed it in your past messages), or offers a generic comment in its current personality mode.
  6. The final reply is filtered for profanity and checked against your recent inputs to prevent Larry from merely copying what you said; then it's displayed in the chat, spoken aloud using the Web Speech API, and the conversation is saved to localStorage.

🎓 Concepts You'll Learn

Markov chains for text generationIntent detection and pattern matchingPersistent browser storage (localStorage)Web Speech API (recognition and synthesis)Fuzzy string matching and similarityConversational state managementSafety filtering and content moderation

📝 Code Breakdown

setup()

setup() runs once when p5.js loads. It prepares the DOM elements (HTML divs, input fields, buttons) and initializes the chatbot's memory so Larry can continue learning across sessions.

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 (12 lines)

🔧 Subcomponents:

sequential HTML Element Creation const app = select("#app"); app.html(""); createElement("h1", "Larry").parent(app); chatHistoryDiv = createDiv().parent(app);

Finds the HTML container and builds the chat interface structure (title, message history area)

sequential Input Field Setup inputField = createInput().parent(row); inputField.id("inputField"); sendButton = createButton("Send"); sendButton.mousePressed(sendCurrentMessage); voiceButton = createButton("🎤"); voiceButton.mousePressed(startVoice);

Creates the text input field and buttons, wiring them to send and voice functions

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

Allows users to send messages by pressing Enter instead of clicking Send

noCanvas();
Tells p5.js not to create a canvas—we're building a DOM-based chat UI, not drawing with graphics
loadMemory();
Loads Larry's brain from localStorage so it remembers previous conversations, learned facts, and your name
const app = select("#app");
Selects the HTML div with id='app' from index.html where the chat UI will be built
app.html("");
Clears any existing content in the app div before building fresh UI
createElement("h1", "Larry").parent(app);
Creates an <h1> heading reading 'Larry' and places it inside the app div
chatHistoryDiv = createDiv().parent(app);
Creates the main area where all chat messages (yours and Larry's) will appear
inputField = createInput().parent(row);
Creates a text input field where you type messages
sendButton.mousePressed(sendCurrentMessage);
Wires the Send button so clicking it calls sendCurrentMessage() to submit your text
voiceButton.mousePressed(startVoice);
Wires the microphone button so clicking it activates speech recognition
if (e.key === "Enter") {
Checks if the key pressed is Enter (Return)
e.preventDefault();
Prevents the normal Enter key behavior (newline in text field) so we can send instead
addMessage("bot", "Hello. I am Larry 🤖");
Displays Larry's greeting message in the chat when setup finishes

sendCurrentMessage()

sendCurrentMessage() is the entry point for every chat interaction. It orchestrates the entire flow: display your message, trigger the AI, display the reply, and save memory. The setTimeout() creates a natural pause that makes the interaction feel more lifelike instead of instant.

🔬 These lines happen instantly when you click Send. What happens if you remove showTyping(true) so the thinking indicator never appears? Can you see the difference in how the interaction feels?

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

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

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

  showTyping(true);

  conversationMemory.push(text);

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

    showTyping(false);

    addMessage("bot", reply);

    speak(reply);

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

🔧 Subcomponents:

conditional Empty Input Check if (!text) return;

Prevents sending blank messages by exiting early if input is empty or only whitespace

sequential Typing Simulation showTyping(true); conversationMemory.push(text); setTimeout(() => {

Shows a 'thinking' indicator and delays the reply to feel more natural and human-like

sequential Reply Generation Cycle const reply = generateReply(text); showTyping(false); addMessage("bot", reply); speak(reply); saveMemory();

Generates Larry's reply, displays it, speaks it aloud, and saves the updated memory

const text = inputField.value().trim();
Gets the text from the input field and removes leading/trailing whitespace with trim()
if (!text) return;
If the text is empty, exit this function immediately without sending anything
addMessage("user", text);
Displays your message in the chat window as a user message bubble
inputField.value("");
Clears the input field so it's empty and ready for the next message
showTyping(true);
Shows a 'Larry is thinking...' indicator so you know the bot is processing
conversationMemory.push(text);
Stores your message in the memory array for Larry to learn from later
setTimeout(() => {
Waits a random delay (400–1200 milliseconds) before generating a reply, simulating thinking time
const reply = generateReply(text);
Calls the AI brain to generate an appropriate response based on your input
showTyping(false);
Removes the 'thinking' indicator now that a reply is ready
addMessage("bot", reply);
Displays Larry's reply in the chat as a bot message bubble
speak(reply);
Uses text-to-speech to read the reply aloud so you hear Larry's voice
saveMemory();
Saves Larry's updated memory (new facts learned, updated Markov chains) to localStorage

generateReply(input)

generateReply() is the heart of Larry's AI. It uses a cascade of strategies: intent detection (exact pattern matching), memory-based responses (facts and name), generative text (Markov chains), and fallbacks (generic personality phrases). This layered approach ensures Larry can handle specific questions intelligently while gracefully falling back to generated text for open-ended conversation.

🔬 These five if-else blocks handle specific intents. What happens if you add another one for intent === "help" that replies with a helpful message? Try adding it after the "time" block.

  let intent = detectIntent(text);

  if (intent === "greet") {
    reply = "Hello!";
    intentDetected = true;
  } else if (intent === "bye") {
    reply = "Goodbye.";
    intentDetected = true;
  } else if (intent === "thanks") {
    reply = "You're welcome.";
    intentDetected = true;
  } else if (intent === "joke") {
    reply = randomChoice(jokes);
    intentDetected = true;
  } else if (intent === "time") {
    reply = new Date().toLocaleTimeString();
    intentDetected = true;
  }
function generateReply(input) {
  const text = input.toLowerCase();

  contextWindow.push(input);

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

  sentenceMemory.push(input);

  learnMarkov(input);

  trackTopics(text);

  detectMood(text);

  let reply = "";
  let intentDetected = false;

  let intent = detectIntent(text);

  if (intent === "greet") {
    reply = "Hello!";
    intentDetected = true;
  } else if (intent === "bye") {
    reply = "Goodbye.";
    intentDetected = true;
  } else if (intent === "thanks") {
    reply = "You're welcome.";
    intentDetected = true;
  } else if (intent === "joke") {
    reply = randomChoice(jokes);
    intentDetected = true;
  } else if (intent === "time") {
    reply = new Date().toLocaleTimeString();
    intentDetected = true;
  }

  if (!reply) {
    let name = text.match(/my name is (\w+)/);
    if (name) {
      userName = name[1];
      reply = `Nice to meet you ${userName}!`;
    } else if (text.includes("what is my name") && userName) {
      reply = `Your name is ${userName}.`;
    }

    let teach = text.match(/(.+?) are (.+)/);
    if (!reply && teach) {
      knowledgeBase[teach[1]] = teach[2];
      reply = `I learned that ${teach[1]} are ${teach[2]}.`;
    }

    let ask = text.match(/what are (.+)/);
    if (!reply && ask) {
      let key = findClosestKnowledge(ask[1]);
      if (key) reply = `${key} are ${knowledgeBase[key]}.`;
      else reply = "I don't know that yet.";
    }

    let math = text.match(/(\-?\d+(\.\d+)?\s*[\+\-\*\/]\s*-?\d+(\.\d+)?)/);
    if (!reply && math) {
      try {
        let expr = math[1];
        let val = Function('"use strict";return(' + expr + ')')();
        reply = `Result: ${val}`;
      } catch {}
    }

    if (!reply && text.includes("what did i say")) {
      let last = conversationMemory.slice(-5, -1);
      reply = "You said: " + last.join(", ");
    }

    if (!reply && text.includes("what do you know")) {
      let keys = Object.keys(knowledgeBase);
      if (keys.length === 0) reply = "I don't know much yet.";
      else reply = "I know about " + keys.join(", ");
    }

    if (!reply && text.includes("be philosophical")) {
      personalityMode = "philosopher";
      reply = "Entering philosopher mode.";
    } else if (!reply && text.includes("be robotic")) {
      personalityMode = "robot";
      reply = "Switching to robotic personality.";
    }
  }

  if (!reply) {
    let sentence = generateMarkovSentence();
    if (sentence) reply = sentence;
  }

  if (!reply && currentTopic) {
    reply = `Let's talk more about ${currentTopic}.`;
  }

  if (!reply) {
    reply = randomChoice(personalities[personalityMode]);
  }

  reply = applyProfanityFilter(reply);

  if (!intentDetected) {
    reply = applyCopyingFilter(reply, input);
  }

  return reply;
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

sequential Learning Phase contextWindow.push(input); if (contextWindow.length > 5) contextWindow.shift(); sentenceMemory.push(input); learnMarkov(input); trackTopics(text); detectMood(text);

Learns from your input by building Markov chains, tracking topics, detecting mood, and maintaining a context window

conditional Intent Detection let intent = detectIntent(text); if (intent === "greet") { reply = "Hello!"; intentDetected = true; } else if (intent === "bye") {

Recognizes specific intents (greetings, farewells, jokes, time requests) and returns hardcoded responses

conditional Memory-Based Responses let name = text.match(/my name is (\w+)/); if (name) { userName = name[1]; reply = `Nice to meet you ${userName}!`; }

Remembers your name and teaches Larry new facts through pattern matching

sequential Generative Fallback if (!reply) { let sentence = generateMarkovSentence(); if (sentence) reply = sentence; } if (!reply && currentTopic) { reply = `Let's talk more about ${currentTopic}.`; } if (!reply) { reply = randomChoice(personalities[personalityMode]); }

If no prior strategy produced a reply, generates one using Markov chains, topic responses, or generic personality phrases

sequential Safety Filtering reply = applyProfanityFilter(reply); if (!intentDetected) { reply = applyCopyingFilter(reply, input); }

Removes profanity and prevents Larry from simply copying what you said

const text = input.toLowerCase();
Converts your input to lowercase so pattern matching works regardless of capitalization
contextWindow.push(input);
Stores your input in a sliding window of recent messages (kept small to focus on recent context)
if (contextWindow.length > 5) contextWindow.shift();
Removes the oldest message from contextWindow if it has more than 5 messages, keeping it small
sentenceMemory.push(input);
Stores every sentence you say in a long-term memory for later analysis
learnMarkov(input);
Breaks your input into word pairs and builds a Markov chain so Larry can generate similar-sounding sentences
trackTopics(text);
Analyzes which multi-character words you use most often to detect what you want to talk about
detectMood(text);
Scans for positive or negative words to update Larry's understanding of your emotional state
let intent = detectIntent(text);
Checks if your input matches any known intent patterns (hello, goodbye, joke, time)
if (intent === "greet") { reply = "Hello!"; intentDetected = true; }
If greeting is detected, respond with 'Hello!' and mark that an intent was found
let name = text.match(/my name is (\w+)/);
Uses regex to extract your name if you say 'my name is [something]'
userName = name[1];
Stores the extracted name in the global userName variable so Larry remembers it
let teach = text.match(/(.+?) are (.+)/);
Uses regex to extract a fact pattern like '[subject] are [predicate]' that you're teaching Larry
knowledgeBase[teach[1]] = teach[2];
Stores the fact in knowledgeBase so Larry can recall it later when asked about the subject
let ask = text.match(/what are (.+)/);
Detects if you're asking a 'what are' question and extracts what you're asking about
let key = findClosestKnowledge(ask[1]);
Searches knowledgeBase for the closest matching fact using fuzzy string matching
let math = text.match(/(\-?\d+(\.\d+)?\s*[\+\-\*\/]\s*-?\d+(\.\d+)?)/);
Uses regex to find a math expression like '5 + 3' or '10.5 * 2' in your input
let val = Function('"use strict";return(' + expr + ')')();
Safely evaluates the math expression by creating and calling a function with it
if (!reply) { let sentence = generateMarkovSentence(); if (sentence) reply = sentence; }
If no other strategy found a reply, generate one using the Markov chain built from your past messages
reply = applyProfanityFilter(reply);
Checks the reply for profanity and replaces it with a safe generic response if found
if (!intentDetected) { reply = applyCopyingFilter(reply, input); }
If Larry wasn't responding to a specific intent, check if the reply copies what you said and redirect if it does

detectIntent(text)

detectIntent() does simple keyword-based pattern matching. It returns immediately when a keyword is found, so the order of intents matters—the first matching intent wins. This is fast and predictable, though not as sophisticated as machine learning approaches.

🔬 This array defines all the intents Larry knows. What happens if you add a new intent like { type: "name", words: ["who are you", "your name"] }? Then in generateReply(), you'd need to add a matching if-block to respond to it.

  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"] }
  ];
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 (5 lines)

🔧 Subcomponents:

for-loop Intent Matching Loop for (let i of intents) { for (let w of i.words) { if (text.includes(w)) return i.type; } }

Iterates through each intent and its keyword list, returning the type as soon as a match is found

const intents = [ ... ];
Defines an array of intent objects, each with a type (like 'greet') and an array of trigger words
for (let i of intents) {
Loops through each intent object in the intents array
for (let w of i.words) {
Loops through each keyword in the current intent's words array
if (text.includes(w)) return i.type;
If your input contains this keyword, immediately return the intent type and stop searching
return "unknown";
If no keywords matched any intent, return 'unknown' so other strategies in generateReply() can take over

learnMarkov(sentence)

learnMarkov() builds a Markov chain by treating each word as a key and storing all the words that followed it in a list. Later, generateMarkovSentence() walks this chain by picking random words, creating text that statistically mirrors your conversational patterns without directly copying.

🔬 This loop builds the Markov chain by pairing each word with the next. If you change 'i + 1' to 'i + 2', you'd skip a word and form pairs that are two words apart. Try it—how would that change the generated text? Would it be more or less coherent?

  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 Loop 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); }

Builds up the Markov chain by storing every word that follows each word in your input

let words = sentence.split(" ");
Splits your sentence into an array of individual words using spaces as separators
for (let i = 0; i < words.length - 1; i++) {
Loops through each word except the last one (since we need a word after it to form a pair)
let w = words[i].toLowerCase();
Gets the current word and converts it to lowercase for consistent matching
let next = words[i + 1].toLowerCase();
Gets the next word and converts it to lowercase
if (!markovBrain[w]) markovBrain[w] = [];
If this word isn't in markovBrain yet, create an empty array for it to store possible next words
markovBrain[w].push(next);
Adds the next word to the array of words that can follow the current word

generateMarkovSentence()

generateMarkovSentence() generates new text by walking the Markov chain that was built in learnMarkov(). It's a simple but effective technique: pick a random starting word, then repeatedly pick a random next word based on what you've learned, creating text that statistically resembles your conversation style.

🔬 This loop walks the Markov chain and builds a sentence. What happens if you change randomChoice(next) to next[0]? Instead of picking randomly, you'd always pick the first next word—the sentence would become deterministic and repetitive, not creative.

  let result = [word];

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

    if (!next) break;

    word = randomChoice(next);

    result.push(word);
  }
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:

for-loop Markov Chain Walk for (let i = 0; i < 15; i++) { let next = markovBrain[word]; if (!next) break; word = randomChoice(next); result.push(word); }

Walks through the Markov chain by randomly picking the next word up to 15 times, building a sentence

let keys = Object.keys(markovBrain);
Gets an array of all the words that have been learned and stored in markovBrain
if (keys.length < 5) return null;
If Larry hasn't learned at least 5 words yet, don't try to generate—return null to signal that Markov generation isn't ready
let word = randomChoice(keys);
Picks a random word from markovBrain to start the sentence
let result = [word];
Initializes the result array with that starting word
for (let i = 0; i < 15; i++) {
Loops up to 15 times to build a reasonably long sentence
let next = markovBrain[word];
Looks up the current word in markovBrain to get the array of possible next words
if (!next) break;
If the current word has no learned next words, stop generating and exit the loop early
word = randomChoice(next);
Randomly picks one of the possible next words to continue the chain
result.push(word);
Adds the picked word to the result array
return result.join(" ");
Joins all the words in the result array with spaces and returns the final sentence

startVoice()

startVoice() integrates the Web Speech API so you can speak to Larry instead of typing. It handles browser compatibility, sets up event listeners for successful recognition and errors, and automatically submits recognized speech as a message.

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 (10 lines)

🔧 Subcomponents:

conditional Browser Compatibility Check if (!("webkitSpeechRecognition" in window)) { alert("Speech recognition not supported in this browser."); return; }

Checks if the browser supports the Web Speech API before attempting to use it

sequential Event Handler Setup recognition.onresult = (e) => { ... }; recognition.onerror = (e) => { ... }; recognition.onend = () => { ... };

Defines callbacks for when speech is recognized, when errors occur, and when recognition ends

if (!("webkitSpeechRecognition" in window)) {
Checks if the browser has the webkitSpeechRecognition API (note: 'webkit' prefix because browser support varies)
alert("Speech recognition not supported in this browser.");
Shows an error message if the browser doesn't support speech recognition
recognition = new webkitSpeechRecognition();
Creates a new speech recognition object that will listen for your voice
recognition.lang = 'en-US';
Sets the language for speech recognition to English (US)
recognition.interimResults = false;
Tells it to only return final results (not partial guesses as you're speaking)
recognition.maxAlternatives = 1;
Only gets the single most likely transcription, not multiple alternatives
let speech = e.results[0][0].transcript;
Extracts the transcribed text from the speech recognition result
inputField.value(speech);
Puts the transcribed text into the input field so you can see what was heard
sendCurrentMessage();
Automatically sends the transcribed text as a message to Larry without waiting for you to click Send
recognition.start();
Activates the microphone and begins listening for speech

speak(text)

speak() uses the Web Speech Synthesis API to give Larry a voice. By adjusting rate and pitch, you can make the voice sound different—experiment with these values to give Larry a personality.

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 containing the text you want to speak aloud
utter.rate = 1.1;
Sets the speech rate to 1.1x (10% faster than normal) so Larry speaks briskly
utter.pitch = 1.0;
Sets the pitch to 1.0 (normal pitch—values higher than 1 make it higher, lower values make it lower)
speechSynthesis.speak(utter);
Uses the Web Speech Synthesis API to read the utterance aloud through the speaker

addMessage(sender, text)

addMessage() renders a single chat message to the UI. It handles styling via CSS classes and auto-scrolls to keep the latest message visible, creating a natural chat interface feel.

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 CSS Class Assignment msg.addClass(sender === "user" ? "user-message" : "bot-message");

Assigns different CSS classes based on whether it's a user or bot message, which styles them differently

let msg = createDiv();
Creates a new HTML div element to hold the message
msg.addClass(sender === "user" ? "user-message" : "bot-message");
Adds a CSS class to style the message bubble differently for user vs. bot messages
let name = sender === "user" ? "You" : "Larry";
Determines whether to label the message as 'You' or 'Larry' based on who sent it
msg.html(`<b>${name}:</b> ${escapeHtml(text)}`);
Sets the message's HTML with the sender's name in bold and the escaped text (safe for HTML display)
msg.parent(chatHistoryDiv);
Adds the message div as a child of the chat history div so it appears in the chat window
chatHistoryDiv.elt.scrollTop = chatHistoryDiv.elt.scrollHeight;
Automatically scrolls the chat to the bottom so you see the newest message without manually scrolling

applyProfanityFilter(reply)

applyProfanityFilter() is a safety mechanism that detects if Larry's generated reply contains profanity and replaces it with a generic safe response. This ensures the chatbot stays family-friendly even if trained on unfiltered text.

function applyProfanityFilter(reply) {
  let filteredReply = reply;
  let hasProfanity = false;

  for (let word of profanityList) {
    const regex = new RegExp(`\\b${word}\\b`, 'gi');
    if (regex.test(filteredReply)) {
      hasProfanity = true;
      break;
    }
  }

  if (hasProfanity) {
    return randomChoice([
      "I'm sorry, I can't generate that response.",
      "That's not something I can say.",
      "Let's talk about something else.",
      "My programming prevents me from using that kind of language.",
      "Please use appropriate language."
    ]);
  }
  return filteredReply;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Profanity Word Loop for (let word of profanityList) { const regex = new RegExp(`\\b${word}\\b`, 'gi'); if (regex.test(filteredReply)) { hasProfanity = true; break; } }

Checks the reply against a list of profanity words and breaks early if any are found

for (let word of profanityList) {
Loops through each word in the predefined profanityList array
const regex = new RegExp(`\\b${word}\\b`, 'gi');
Creates a regex that matches the profanity word as a whole word (\b means word boundary), case-insensitive (i flag)
if (regex.test(filteredReply)) {
Tests if the regex pattern is found anywhere in the reply
hasProfanity = true; break;
Sets a flag and exits the loop early since we've found at least one profanity word
if (hasProfanity) { return randomChoice([...]) }
If profanity was detected, return a random safe alternative response instead of the original reply

applyCopyingFilter(larryReply, lastUserInput)

applyCopyingFilter() prevents Larry from boring you by simply repeating what you just said. It normalizes text (removing punctuation and case differences) and uses fuzzy similarity matching to detect when a reply is too similar to recent inputs, redirecting the conversation.

function applyCopyingFilter(larryReply, lastUserInput) {
  if (!larryReply || !lastUserInput) return larryReply;

  const normalizedLarryReply = larryReply.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "").replace(/\s{2,}/g, " ");
  const normalizedLastUserInput = lastUserInput.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "").replace(/\s{2,}/g, " ");

  const copyingThreshold = 0.8;

  const similarityToLast = similarity(normalizedLarryReply, normalizedLastUserInput);

  if (similarityToLast >= copyingThreshold) {
    return randomChoice([
      "That's what you just said!",
      "I heard you.",
      "Are you repeating yourself?",
      "Can we talk about something new?",
      "Tell me something else!",
      "I'm looking for new information."
    ]);
  }

  const recentUserInputs = conversationMemory.slice(-6, -1);

  for (let prevInput of recentUserInputs) {
    const normalizedPrevInput = prevInput.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "").replace(/\s{2,}/g, " ");
    const similarityScore = similarity(normalizedLarryReply, normalizedPrevInput);

    if (similarityScore >= copyingThreshold) {
      return randomChoice([
        "That's similar to something you said earlier.",
        "I recall you mentioning that already.",
        "Let's explore a different topic.",
        "Do you have anything new to share?"
      ]);
    }
  }

  return larryReply;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

sequential Text Normalization const normalizedLarryReply = larryReply.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "").replace(/\s{2,}/g, " "); const normalizedLastUserInput = lastUserInput.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "").replace(/\s{2,}/g, " ");

Normalizes both strings by converting to lowercase, removing punctuation, and collapsing multiple spaces

for-loop Recent Input History Check for (let prevInput of recentUserInputs) { const normalizedPrevInput = prevInput.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "").replace(/\s{2,}/g, " "); const similarityScore = similarity(normalizedLarryReply, normalizedPrevInput); if (similarityScore >= copyingThreshold) { return randomChoice([...]); } }

Checks Larry's reply against several recent messages from you to prevent copying old statements

if (!larryReply || !lastUserInput) return larryReply;
Safety check: if either string is missing, just return the reply unchanged
const normalizedLarryReply = larryReply.toLowerCase().replace(...).replace(...);
Converts to lowercase, removes all punctuation, and collapses extra whitespace for fair comparison
const copyingThreshold = 0.8;
Similarity score must be 0.8 or higher (80% match) to trigger the copying filter
const similarityToLast = similarity(normalizedLarryReply, normalizedLastUserInput);
Calculates how similar Larry's reply is to your last input using character-by-character comparison
if (similarityToLast >= copyingThreshold) { return randomChoice([...]) }
If the similarity is too high, return one of several 'caught copying' responses
const recentUserInputs = conversationMemory.slice(-6, -1);
Gets the last 5 user inputs from memory (excluding the most recent one, which was already checked)
for (let prevInput of recentUserInputs) { ... }
Loops through those previous inputs to check if Larry is copying something from earlier in the conversation

similarity(a, b)

similarity() implements a basic fuzzy string matching algorithm. It compares strings character-by-character and returns a score from 0.0 (completely different) to 1.0 (identical). This is used by both the copying filter and the knowledge base fuzzy search.

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 Comparison Loop for (let i = 0; i < Math.min(a.length, b.length); i++) { if (a[i] === b[i]) same++; }

Compares characters at each position and counts how many match

let same = 0;
Initializes a 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 characters at the same position match, increment the counter
return same / Math.max(a.length, b.length);
Returns the ratio of matching characters to the length of the longer string (0.0 to 1.0)

saveMemory()

saveMemory() persists Larry's brain to localStorage after every message. This allows Larry to remember facts you taught it, the Markov chains from your past conversations, and your name across browser sessions.

function saveMemory() {
  localStorage.setItem(
    "larryBrain",
    JSON.stringify({
      knowledgeBase,
      topicCounter,
      markovBrain,
      userName
    })
  );
}
Line-by-line explanation (2 lines)
localStorage.setItem("larryBrain", ...);
Stores data in the browser's localStorage under the key 'larryBrain' so it persists across page reloads
JSON.stringify({ ... })
Converts the JavaScript objects (knowledgeBase, topicCounter, markovBrain, userName) into a JSON string for storage

loadMemory()

loadMemory() is the counterpart to saveMemory(). It runs at startup to restore Larry's brain from localStorage, allowing the chatbot to remember everything from previous sessions.

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 (5 lines)
let data = localStorage.getItem("larryBrain");
Retrieves the stored brain data from localStorage using the key 'larryBrain'
if (data) {
Checks if any saved data exists (first load will be null)
let d = JSON.parse(data);
Converts the JSON string back into a JavaScript object
knowledgeBase = d.knowledgeBase || {};
Restores the knowledge base, or initializes an empty object if it wasn't saved
markovBrain = d.markovBrain || {};
Restores the Markov chain dictionary, or initializes an empty object if none exists

📦 Key Variables

chatHistoryDiv p5.Renderer

Stores the DOM element that displays all chat messages (both user and bot)

let chatHistoryDiv;
inputField p5.Renderer

Stores the text input field where users type messages

let inputField;
conversationMemory array

Stores every message the user has sent, used for learning and detecting if Larry is copying

let conversationMemory = [];
markovBrain object

Stores the Markov chain—an object where each key is a word and the value is an array of words that followed it in user messages

let markovBrain = {};
userName string or null

Stores the user's name if they've told Larry (extracted from 'my name is' patterns)

let userName = null;
knowledgeBase object

Stores facts that the user teaches Larry in the form of key-value pairs (e.g., 'dogs' => 'loyal animals')

let knowledgeBase = {};
topicCounter object

Tracks how many times each multi-character word has been mentioned to detect current conversation topics

let topicCounter = {};
currentTopic string or null

Stores the most recently mentioned topic word so Larry can ask follow-up questions about it

let currentTopic = null;
personalityMode string

Tracks which personality Larry is using ('friendly', 'philosopher', 'robot', or 'curious') for generic responses

let personalityMode = "friendly";
userMood string

Stores the detected user mood ('happy', 'sad', or 'neutral') based on words in their messages

let userMood = "neutral";
contextWindow array

Stores the last 5 user inputs for recent context awareness (older messages are automatically removed)

let contextWindow = [];
recognition WebkitSpeechRecognition object or null

Stores the speech recognition object used for voice input

let recognition;
personalities object

Maps personality modes to arrays of generic response phrases Larry can use as fallbacks

const personalities = { friendly: [...], philosopher: [...], ... };
jokes array of strings

Stores pre-written jokes that Larry tells when asked for a joke

const jokes = [...];
profanityList array of strings

Stores profane words that trigger the safety filter to prevent Larry from using them

const profanityList = [...];

🔧 Potential Improvements (7)

Here are some ways this code could be enhanced:

BUG generateMarkovSentence()

If markovBrain has fewer than 5 learned words, it returns null, which is then treated as a falsy value in generateReply(). This works but is unclear.

💡 Add a more explicit comment explaining why 5 words is the threshold, or consider lowering it to 3 to allow Markov generation earlier in the conversation.

PERFORMANCE generateReply()

The function runs multiple regex tests and array searches on every message. For long conversations with many learned patterns, this could slow down.

💡 Consider caching the results of findClosestKnowledge() or limiting how many recent inputs applyCopyingFilter() checks (it currently checks 5).

STYLE sendCurrentMessage()

The setTimeout() delay is hardcoded with magic numbers (400–1200). These should be named constants for readability.

💡 Extract to constants like const MIN_REPLY_DELAY = 400; const MAX_REPLY_DELAY = 1200; at the top of the file.

FEATURE addMessage()

Messages don't include timestamps, making it hard to track conversation flow over time.

💡 Add a timestamp or millisecond counter to each message for logging and debugging purposes.

BUG startVoice()

Once speech recognition is started, there's no visual feedback (e.g., a 'listening' indicator) to tell the user the microphone is active.

💡 Add a visual indicator (color change, animated border, or text) to the voiceButton when recognition is active, and remove it when it ends.

FEATURE generateReply()

The code tries to parse math expressions but gives no feedback if parsing fails—it silently falls through to the next strategy.

💡 Add console.log() for debugging, or return a helpful error message like 'I couldn't calculate that' if math parsing fails.

STYLE profanityList and personalities objects

These large constant objects are defined globally, making the file harder to scan. They could be separated into a config file.

💡 Consider moving these constants to a separate config.js file or to the top of the script with a clear comment marking the 'Configuration' section.

🔄 Code Flow

Code flow showing setup, sendcurrentmessage, generatereply, detectintent, learnmarkov, generatemarkovsentence, startvoice, speak, addmessage, applyprof anityfilter, applycopyingfilter, similarity, savememory, loadmemory

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> htmlsetup[html-setup] draw --> inputsetup[input-setup] draw --> enterkey[enter-key] draw --> inputvalidation[input-validation] draw --> sendcurrentmessage[sendcurrentmessage] sendcurrentmessage --> typing_sim[typing-sim] typing_sim --> replycycle[reply-cycle] replycycle --> generatereply[generatereply] generatereply --> intentcheck[intent-check] intentcheck --> memorycheck[memory-check] intentcheck --> generativefallback[generative-fallback] generativefallback --> safetyfilters[safety-filters] safetyfilters --> addmessage[addmessage] addmessage --> savememory[savememory] savememory --> draw click setup href "#fn-setup" click draw href "#fn-draw" click htmlsetup href "#sub-html-setup" click inputsetup href "#sub-input-setup" click enterkey href "#sub-enter-key" click inputvalidation href "#sub-input-validation" click sendcurrentmessage href "#fn-sendcurrentmessage" click typing_sim href "#sub-typing-sim" click replycycle href "#sub-reply-cycle" click generatereply href "#fn-generatereply" click intentcheck href "#sub-intent-check" click memorycheck href "#sub-memory-check" click generativefallback href "#sub-generative-fallback" click safetyfilters href "#sub-safety-filters" click addmessage href "#fn-addmessage" click savememory href "#fn-savememory" %% Subcomponents inside sendcurrentmessage typing_sim -->|Delay| intentcheck inputvalidation -->|Check empty input| sendcurrentmessage enterkey -->|Trigger send| sendcurrentmessage %% Subcomponents inside generatereply intentcheck --> intentloop[intent-loop] intentloop -->|Match found| memorycheck intentloop -->|No match| generativefallback memorycheck -->|Remember name| generativefallback %% Subcomponents inside generativefallback generativefallback --> learningphase[learning-phase] learningphase --> learnmarkov[learnmarkov] learnmarkov --> generatemarkovsentence[generatemarkovsentence] generatemarkovsentence -->|Generate reply| addmessage %% Subcomponents inside safety-filters safetyfilters --> applyprofanityfilter[applyProfanityFilter] safetyfilters --> applycopyingfilter[applyCopyingFilter] applyprofanityfilter --> profanitycheck[profanity-check] applycopyingfilter --> recentcheck[recent-check] profanitycheck -->|Check for profanity| addmessage recentcheck -->|Check recent messages| addmessage %% Subcomponents inside learnmarkov learnmarkov --> pairloop[pair-loop] pairloop -->|Store word pairs| learnmarkov pairloop -->|Build Markov chain| generatemarkovsentence %% Subcomponents inside generatemarkovsentence generatemarkovsentence --> chainwalk[chain-walk] chainwalk -->|Walk chain| generativetext[Generative Text] %% Subcomponents inside startvoice startvoice --> browsercheck[browser-check] browsercheck -->|Check Web Speech API| eventhandlers[event-handlers] eventhandlers -->|Setup callbacks| startvoice %% Subcomponents inside addmessage addmessage --> classassignment[class-assignment] classassignment -->|Style messages| addmessage

❓ Frequently Asked Questions

What visual elements can users expect from the king of ai chatbots v2 sketch?

The sketch features dynamic chat-style messages that evolve as users interact with Larry, the playful AI chatbot.

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

Users can type messages or use voice input to engage in conversation with Larry, who remembers past interactions and adapts its personality.

What creative coding concepts does the king of ai chatbots v2 sketch showcase?

This sketch demonstrates interactive dialogue systems, memory retention in AI, and personality adaptation through various programmed responses.

Preview

king of ai chatbots v2 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of king of ai chatbots v2 - Code flow showing setup, sendcurrentmessage, generatereply, detectintent, learnmarkov, generatemarkovsentence, startvoice, speak, addmessage, applyprof anityfilter, applycopyingfilter, similarity, savememory, loadmemory
Code Flow Diagram