potato ai 2.0 OFFICAL

This sketch is a joke chatbot called 'Potato AI 2.0' built entirely with p5.js DOM functions instead of the canvas. You type messages into a text box and a potato-themed 'AI' named larry replies with puns, random meme images, follow-up questions, or glitchy nonsense, plus a few hidden easter eggs like a fake Rickroll and a full-screen 'code chaos' sequence.

🧪 Try This!

Experiment with the code by making these changes:

  1. Rename the AI — Every AI chat bubble is prefixed with the bot's name, so changing this string instantly renames it everywhere.
  2. Make meme images way more common — Raising this probability makes the bot reply with a random potato meme image far more often than plain text.
  3. Make the secret dance-potato event common — This rare 1% easter egg hides the whole chat and shows a full-screen dancing potato GIF - crank the odds up to see it almost every time.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch never touches the canvas at all - it calls noCanvas() and instead builds an entire chat interface out of p5.js DOM helper functions like createDiv(), createButton(), createInput(), and createImg(). Typing a message and pressing Send or Enter triggers a fake 'thinking' delay, after which a rule-based function decides how the potato AI should respond: a scripted joke, a random meme image, a follow-up question, or a totally random one-liner. Along the way it demonstrates event listeners, setTimeout-based timing, and p5's random() function used purely for decision-making rather than visuals.

The code is organized around one big setup() that builds every UI element, a sendMessage() function that handles user input, a generateResponse() function that acts as the chatbot's 'brain' using chained if-statements and random chance, and an addMessage() function that renders each chat bubble (including special cases for images and a rare full-screen event). By studying it you'll learn how to build interactive DOM-based UIs in p5.js, how to fake asynchronous 'thinking' with setTimeout, and how simple probability checks with random() can make a rule-based bot feel unpredictable and alive.

⚙️ How It Works

  1. When the page loads, setup() runs once and builds the whole interface - title, chat window, input box, Send button, and a hidden Settings panel - entirely with p5.js DOM functions rather than drawing on a canvas.
  2. When you submit a message, sendMessage() adds your text to the chat, shows a temporary 'thinking' potato image, and waits 1.5 seconds using setTimeout to simulate the AI 'thinking'.
  3. After the delay, generateResponse() checks your lowercased message against a chain of keyword rules (like 'hello', 'banana', 'fries') and, if none match, rolls random chances to send a meme image, ask a follow-up question, go off-script, or fall back to a default reply.
  4. addMessage() turns whatever generateResponse() returned into a styled chat bubble, with special handling for image replies and a rare 1%-chance full-screen 'dancing potato' event that hides the whole interface.
  5. Toggling 'code chaos' in Settings routes every future AI reply through glitchText(), which randomly flips character case and duplicates letters for a corrupted look, and also plays a Matrix-style digital rain overlay.
  6. Clicking the red X button in the corner never actually closes anything - it swaps in a full-screen Rickroll overlay instead, permanently hiding the chat UI until the page reloads.

🎓 Concepts You'll Learn

DOM manipulation with p5.js (createDiv, createButton, createImg)Event-driven programming (mousePressed, changed, keypress listeners)Conditional logic trees for a simple rule-based chatbotRandomness with random() used to control probabilities, not visualsAsynchronous timing with setTimeout to fake 'thinking'String manipulation and template building

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. In DOM-based p5.js sketches like this one, setup() is where you build your entire interface using createDiv(), createButton(), createInput() and similar functions, then wire up event handlers with .mousePressed(), .changed(), and .input().

🔬 This toggles the settings panel by flipping settingsVisible true/false each click. What happens if you change "block" to "flex" in the true case - does the panel look any different, and why might 'flex' matter for panels with rows of controls?

  settingsButton.mousePressed(() => {
    settingsVisible = !settingsVisible;
    settingsPanel.style("display", settingsVisible ? "block" : "none");
  });
function setup() {
  noCanvas();

  // --- Create Main Flex Container ---
  mainContainer = createDiv();
  mainContainer.style("display", "flex");
  mainContainer.style("flex-direction", "column");
  mainContainer.style("height", "100vh"); // Make it full height

  // --- Create the Title Element ---
  titleElement = createElement('h1', 'Potato AI 2.0 🥔');
  titleElement.style("text-align", "center");
  titleElement.style("padding", "10px");
  titleElement.style("margin", "0");
  titleElement.style("background-color", "#f5e0b3");
  titleElement.style("border-bottom", "1px solid #d2b48c");
  titleElement.parent(mainContainer);

  // --- Subtitle under the title ---
  subtitleElement = createElement('p', '©️CDWW inc 2026');
  subtitleElement.style("text-align", "center");
  subtitleElement.style("margin", "0 0 8px 0");
  subtitleElement.style("padding", "2px 0 6px 0");
  subtitleElement.style("font-size", "12px");
  subtitleElement.style("background-color", "#f5e0b3");
  subtitleElement.style("border-bottom", "1px solid #d2b48c");
  subtitleElement.parent(mainContainer);

  // --- Controls area (Settings + Play GIF) ---
  let controlsArea = createDiv();
  controlsArea.style("display", "flex");
  controlsArea.style("justify-content", "center");
  controlsArea.style("gap", "10px");
  controlsArea.style("padding", "8px");
  controlsArea.style("background-color", "#f3d6a1");
  controlsArea.parent(mainContainer);

  settingsButton = createButton("Settings");
  settingsButton.style("padding", "6px 12px");
  settingsButton.style("border-radius", "8px");
  settingsButton.style("border", "1px solid #d2b48c");
  settingsButton.style("background-color", "#fff3cd");
  settingsButton.style("cursor", "pointer");
  settingsButton.parent(controlsArea);

  playGifButton = createButton("Play Potato GIF");
  playGifButton.style("padding", "6px 12px");
  playGifButton.style("border-radius", "8px");
  playGifButton.style("border", "1px solid #d2b48c");
  playGifButton.style("background-color", "#ffe4b5");
  playGifButton.style("cursor", "pointer");
  playGifButton.parent(controlsArea);

  // --- Chat Container ---
  chatContainer = createDiv();
  chatContainer.style("flex-grow", "1"); // Make chatContainer fill available space
  chatContainer.style("overflow-y", "auto");
  chatContainer.style("padding", "10px");
  chatContainer.style("display", "flex");
  chatContainer.style("flex-direction", "column");
  chatContainer.style("background-color", "#f5e0b3");
  chatContainer.parent(mainContainer); // Parent to mainContainer

  // --- Input Area ---
  inputArea = createDiv();
  inputArea.style("display", "flex");
  inputArea.style("padding", "10px");
  inputArea.style("background-color", "#f5e0b3");
  inputArea.parent(mainContainer); // Parent to mainContainer

  inputBox = createInput();
  inputBox.style("flex", "1");
  inputBox.style("padding", "10px");
  inputBox.style("font-size", "16px");
  inputBox.parent(inputArea);

  sendButton = createButton("Send");
  sendButton.style("padding", "10px 20px");
  sendButton.style("margin-left", "5px");
  sendButton.style("border-radius", "8px");
  sendButton.style("border", "1px solid #d2b48c");
  sendButton.style("background-color", "#fff3cd");
  sendButton.parent(inputArea);

  sendButton.mousePressed(sendMessage);

  // Store the listener function
  keypressListener = function(e) {
    if (e.key === "Enter") sendMessage();
  };
  inputBox.elt.addEventListener("keypress", keypressListener);

  // --- Settings panel (hidden by default) ---
  settingsPanel = createDiv();
  settingsPanel.style("position", "fixed");
  settingsPanel.style("top", "60px");
  settingsPanel.style("right", "10px");
  settingsPanel.style("padding", "10px");
  settingsPanel.style("background-color", "#fffaf0");
  settingsPanel.style("border", "1px solid #d2b48c");
  settingsPanel.style("border-radius", "8px");
  settingsPanel.style("box-shadow", "0 2px 6px rgba(0,0,0,0.15)");
  settingsPanel.style("z-index", "900");
  settingsPanel.style("display", "none");
  settingsPanel.parent(document.body);

  let settingsTitle = createElement('h3', 'Settings');
  settingsTitle.style("margin", "0 0 8px 0");
  settingsTitle.style("font-size", "16px");
  settingsTitle.parent(settingsPanel);

  smartModeCheckbox = createCheckbox(' Smart mode', false);
  smartModeCheckbox.parent(settingsPanel);
  smartModeCheckbox.style("margin-bottom", "8px");
  smartModeCheckbox.changed(function() {
    smartModeEnabled = smartModeCheckbox.checked();
    if (smartModeEnabled) {
      addMessage("Smart mode activated. This potato is now extra crispy 🥔✨", "ai");
    } else {
      addMessage("Smart mode deactivated. Back to simple spud life.", "ai");
    }
  });

  let bgLabel = createP('Background color');
  bgLabel.style("margin", "8px 0 4px 0");
  bgLabel.style("font-size", "14px");
  bgLabel.parent(settingsPanel);

  bgColorPicker = createColorPicker('#f5e0b3');
  bgColorPicker.parent(settingsPanel);
  bgColorPicker.style("width", "100%");
  bgColorPicker.input(function() {
    const c = bgColorPicker.value();
    document.body.style.backgroundColor = c;
    if (chatContainer) {
      chatContainer.style('background-color', c);
    }
    if (inputArea) {
      inputArea.style('background-color', c);
    }
  });

  // NEW: "Mess with Potato AI's code" button
  let chaosLabel = createP('Danger zone: mess with Potato AI');
  chaosLabel.style("margin", "10px 0 4px 0");
  chaosLabel.style("font-size", "13px");
  chaosLabel.style("color", "#b00000");
  chaosLabel.parent(settingsPanel);

  codeChaosButton = createButton("Mess with Potato AI's code");
  codeChaosButton.parent(settingsPanel);
  codeChaosButton.style("width", "100%");
  codeChaosButton.style("margin-top", "4px");
  codeChaosButton.style("padding", "6px");
  codeChaosButton.style("background-color", "#ffcccc");
  codeChaosButton.style("border", "1px solid #d2b48c");
  codeChaosButton.style("border-radius", "6px");
  codeChaosButton.mousePressed(() => {
    codeChaosEnabled = !codeChaosEnabled;
    if (codeChaosEnabled) {
      addMessage("Uh oh... you just messed with my code. Expect some weird glitches now.", "ai");
      codeChaosButton.html("Fix Potato AI's code");
      showCodeChaosSequence(); // trigger GIF then static image
    } else {
      addMessage("Okay, I think my code is back to normal. I hope.", "ai");
      codeChaosButton.html("Mess with Potato AI's code");
    }
  });

  // Toggle settings panel visibility
  settingsButton.mousePressed(() => {
    settingsVisible = !settingsVisible;
    settingsPanel.style("display", settingsVisible ? "block" : "none");
  });

  // Play GIF button behavior
  playGifButton.mousePressed(handlePlayGifButton);

  // --- Add the X Button (now Rickroll button) ---
  let exitButton = createButton('X');
  exitButton.style('position', 'fixed');
  exitButton.style('top', '10px');
  exitButton.style('right', '10px');
  exitButton.style('padding', '10px 15px');
  exitButton.style('font-size', '18px');
  exitButton.style('background-color', '#ff0000'); // Red for exit
  exitButton.style('color', 'white');
  exitButton.style('border', 'none');
  exitButton.style('border-radius', '5px');
  exitButton.style('cursor', 'pointer');
  exitButton.style('z-index', '1000'); // Ensure it's on top of other elements

  // Instead of leaving, X now Rickrolls you
  exitButton.mousePressed(showRickrollOverlay);
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

conditional Smart Mode Toggle Handler if (smartModeEnabled) { addMessage("Smart mode activated..."); } else { addMessage("Smart mode deactivated..."); }

Announces in the chat whenever smart mode is switched on or off

conditional Code Chaos Toggle Handler if (codeChaosEnabled) { ... showCodeChaosSequence(); } else { ... }

Flips glitch mode on/off and launches the Matrix-style overlay sequence the first time it's turned on

conditional Settings Panel Toggle settingsPanel.style("display", settingsVisible ? "block" : "none");

Shows or hides the settings panel based on the settingsVisible boolean

noCanvas();
Tells p5.js not to create a drawing canvas at all, since this whole sketch is built from HTML elements instead.
mainContainer = createDiv();
Creates the outer wrapper div that everything else (title, chat, input) gets attached to.
mainContainer.style("height", "100vh");
Forces the wrapper to fill the full height of the browser viewport.
titleElement = createElement('h1', 'Potato AI 2.0 🥔');
Creates an actual <h1> heading element with the sketch's title text.
chatContainer.style("flex-grow", "1");
Makes the chat area expand to fill all remaining vertical space, pushing the input bar to the bottom.
sendButton.mousePressed(sendMessage);
Wires the Send button so clicking it calls sendMessage().
keypressListener = function(e) { if (e.key === "Enter") sendMessage(); };
Stores a function (so it can be removed later) that calls sendMessage() whenever the Enter key is pressed.
inputBox.elt.addEventListener("keypress", keypressListener);
Attaches the raw DOM event listener to the underlying input element, since p5 doesn't have a built-in 'Enter key' handler.
settingsPanel.style("display", "none");
Hides the settings panel by default until the Settings button is clicked.
smartModeCheckbox.changed(function() { ... });
Registers a callback that runs whenever the checkbox is ticked or unticked, updating smartModeEnabled and posting a chat message.
bgColorPicker.input(function() { ... });
Runs live as the user drags the color picker, instantly updating the page and chat background colors.
codeChaosButton.mousePressed(() => { ... });
Toggles the codeChaosEnabled flag and either starts the chaos sequence or announces things are back to normal.
settingsButton.mousePressed(() => { settingsVisible = !settingsVisible; ... });
Flips a boolean each click and shows/hides the settings panel to match.
exitButton.mousePressed(showRickrollOverlay);
Instead of closing anything, clicking the red X button launches the Rickroll overlay function.

sendMessage()

sendMessage() shows the classic pattern for faking asynchronous 'AI thinking' in a synchronous language: display a loading indicator, use setTimeout() to wait, then remove the indicator and show the real result inside the callback.

🔬 This is a 1-in-100 chance of the rare dark screen event. What happens if you change 0.01 to 0.5 - how often does the easter egg show up now, and does it feel less special?

    // 1% chance: screen goes dark and show dancing potato GIF
    if (random() < 0.01) { // 1% chance
function sendMessage() {
  let message = inputBox.value();
  if (message.trim() === "") return;

  addMessage(message, "user");
  inputBox.value("");

  // Create and display thinking indicator (potato themed)
  thinkingCowImageElement = createImg(
    "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTYF52yg3wYtcIfyH87v3OxJkniCeSNMyMYIg&s",
    "Potato thinking"
  );
  thinkingCowImageElement.style("width", "80px");
  thinkingCowImageElement.style("height", "auto");
  thinkingCowImageElement.style("margin", "5px 0");
  thinkingCowImageElement.style("align-self", "flex-start");
  thinkingCowImageElement.style("border-radius", "10px");
  thinkingCowImageElement.parent(chatContainer);
  thinkingCowImageElement.elt.scrollIntoView({ behavior: 'smooth', block: 'end' });

  // Thinking delay (1.5 seconds)
  setTimeout(() => {
    // Remove thinking indicator
    if (thinkingCowImageElement) {
      thinkingCowImageElement.remove();
      thinkingCowImageElement = null; // Clear the reference
    }

    // 1% chance: screen goes dark and show dancing potato GIF
    if (random() < 0.01) { // 1% chance
      addMessage(
        {
          type: "darkScreen",
          gifSrc: "https://media.tenor.com/6cy01cOku4kAAAAM/dancing-potato.gif"
        },
        "ai"
      );
      return; // Stop processing other responses
    }

    let reply = generateResponse(message);
    addMessage(reply, "ai"); // larry's name will be added inside addMessage
  }, 1500);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Empty Message Guard if (message.trim() === "") return;

Stops the function immediately if the user tries to send a blank message

conditional Remove Thinking Indicator if (thinkingCowImageElement) { thinkingCowImageElement.remove(); thinkingCowImageElement = null; }

Deletes the temporary 'thinking' image once the fake delay is over

conditional 1% Dark Screen Easter Egg if (random() < 0.01) { ... }

Rare random chance to trigger the full-screen dancing potato GIF event instead of a normal reply

let message = inputBox.value();
Reads whatever text is currently typed into the input box.
if (message.trim() === "") return;
Ignores the Send action entirely if the box is empty or just whitespace.
addMessage(message, "user");
Immediately posts the user's own message as a chat bubble on the right side.
inputBox.value("");
Clears the text box so it's ready for the next message.
thinkingCowImageElement = createImg( ... );
Creates a small 'thinking' image and drops it into the chat while the fake AI 'thinks'.
thinkingCowImageElement.elt.scrollIntoView({ behavior: 'smooth', block: 'end' });
Smoothly scrolls the chat window down so the newest bubble (the thinking image) is visible.
setTimeout(() => { ... }, 1500);
Waits 1500 milliseconds (1.5 seconds) before running the code inside, simulating the AI 'thinking' before it replies.
if (thinkingCowImageElement) { thinkingCowImageElement.remove(); thinkingCowImageElement = null; }
Removes the thinking image from the page once the delay is done, and clears the reference so it can't be removed twice.
if (random() < 0.01) { ... }
1% of the time, instead of a normal reply, this fires the rare full-screen dancing potato GIF event and exits early.
let reply = generateResponse(message);
Asks the 'brain' function to decide what larry should say back, based on the user's message.
addMessage(reply, "ai");
Posts whatever generateResponse() returned (text or an image) as an AI chat bubble.

handlePlayGifButton()

This function shows how a simple counter variable (playGifClickCount) can drive an escalating joke, and how String.repeat() is a handy way to build repetitive text without a manual loop.

🔬 What happens if you swap .repeat(playGifClickCount) for a fixed number like .repeat(5) - does the phrase still grow with each click, or does it stay the same length forever?

    const phraseBase = "are you ";
    const sures = ("sure ".repeat(playGifClickCount)).trim(); // sure, sure sure, sure sure sure...
    const phrase = phraseBase + sures;
function handlePlayGifButton() {
  const gifUrl = "https://media.tenor.com/6cy01cOku4kAAAAM/dancing-potato.gif";

  if (playGifClickCount === 0) {
    // First time: open GIF and Potato AI panics
    window.open(gifUrl, "_blank");
    addMessage("NOOO DONT LEAVE ME", "ai");
    playGifClickCount++;
  } else {
    // Subsequent clicks: "are you sure", "are you sure sure", ...
    const phraseBase = "are you ";
    const sures = ("sure ".repeat(playGifClickCount)).trim(); // sure, sure sure, sure sure sure...
    const phrase = phraseBase + sures;

    const confirmed = window.confirm(phrase + "?");
    addMessage(phrase, "ai");

    if (confirmed) {
      playGifClickCount++;
      // If they say yes, open the GIF again
      window.open(gifUrl, "_blank");
    }
    // If they cancel, we don't change the count,
    // so the same level of "sure" repeats next time.
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional First Click vs Repeat Clicks if (playGifClickCount === 0) { ... } else { ... }

Runs a different, simpler behavior on the very first click compared to every click after that

calculation Building the escalating 'sure' phrase const sures = ("sure ".repeat(playGifClickCount)).trim();

Repeats the word 'sure' once per previous click so the confirmation question gets longer and more absurd each time

conditional Confirm Dialog Check if (confirmed) { playGifClickCount++; window.open(gifUrl, "_blank"); }

Only increments the counter and reopens the GIF if the user clicks OK on the browser confirm dialog

const gifUrl = "https://media.tenor.com/6cy01cOku4kAAAAM/dancing-potato.gif";
Stores the dancing potato GIF link so it can be reused in multiple places below.
if (playGifClickCount === 0) {
Checks whether this is the very first time the button has been clicked.
window.open(gifUrl, "_blank");
Opens the GIF in a brand new browser tab.
addMessage("NOOO DONT LEAVE ME", "ai");
Posts a dramatic in-character message reacting to the user opening the GIF.
const sures = ("sure ".repeat(playGifClickCount)).trim();
Uses String.repeat() to build a string like 'sure', 'sure sure', 'sure sure sure' based on how many times the button was previously clicked.
const confirmed = window.confirm(phrase + "?");
Pops up the browser's native OK/Cancel confirm dialog with the escalating phrase as the question.
if (confirmed) { playGifClickCount++; window.open(gifUrl, "_blank"); }
Only if the user clicks OK does the counter go up and the GIF reopen; clicking Cancel leaves everything unchanged.

showRickrollOverlay()

This function demonstrates a full-screen takeover pattern common in web pranks: create a fixed-position div with a high z-index that covers the whole viewport, then stop other event listeners from interfering.

function showRickrollOverlay() {
  // Only create it once
  if (rickrollOverlay) return;

  // Hide main UI
  if (mainContainer) {
    mainContainer.style("display", "none");
  }
  if (settingsPanel) {
    settingsPanel.style("display", "none");
  }
  document.body.style.backgroundColor = "black";

  // Stop Enter key sending messages
  if (inputBox && keypressListener) {
    inputBox.elt.removeEventListener("keypress", keypressListener);
  }

  // Fullscreen overlay
  rickrollOverlay = createDiv();
  rickrollOverlay.style("position", "fixed");
  rickrollOverlay.style("top", "0");
  rickrollOverlay.style("left", "0");
  rickrollOverlay.style("width", "100vw");
  rickrollOverlay.style("height", "100vh");
  rickrollOverlay.style("display", "flex");
  rickrollOverlay.style("flex-direction", "column");
  rickrollOverlay.style("justify-content", "center");
  rickrollOverlay.style("align-items", "center");
  rickrollOverlay.style("background-color", "black");
  rickrollOverlay.style("z-index", "999");
  rickrollOverlay.parent(document.body);

  let text = createP("lol you got Rick rolled 3 years later");
  text.style("color", "white");
  text.style("font-size", "24px");
  text.style("margin", "0 0 20px 0");
  text.style("text-align", "center");
  text.parent(rickrollOverlay);

  let gif = createImg(
    "https://media.tenor.com/x8v1oNUOmg4AAAAM/rickroll-roll.gif",
    "Rickroll"
  );
  gif.style("max-width", "90%");
  gif.style("max-height", "80%");
  gif.style("width", "auto");
  gif.style("height", "auto");
  gif.parent(rickrollOverlay);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Run-Once Guard if (rickrollOverlay) return;

Prevents building duplicate overlapping overlays if the X button gets clicked more than once

if (rickrollOverlay) return;
If the overlay already exists, exit immediately so it's never created twice.
mainContainer.style("display", "none");
Hides the entire chat interface so only the Rickroll overlay is visible.
inputBox.elt.removeEventListener("keypress", keypressListener);
Detaches the Enter-key listener so pressing Enter can no longer send messages once you're rickrolled.
rickrollOverlay = createDiv();
Creates a new div that will become the full-screen black overlay.
rickrollOverlay.style("z-index", "999");
Places the overlay above the normal page content so it fully covers everything underneath.
let gif = createImg( ... );
Loads and displays the actual Rickroll GIF image inside the overlay.

showCodeChaosSequence()

This function chains two timed visual stages together using createImg() and setTimeout(), a simple way to build a mini 'cutscene' using only DOM elements.

function showCodeChaosSequence() {
  // Only run once
  if (codeChaosOverlay) return;

  codeChaosOverlay = createDiv();
  codeChaosOverlay.style("position", "fixed");
  codeChaosOverlay.style("top", "0");
  codeChaosOverlay.style("left", "0");
  codeChaosOverlay.style("width", "100vw");
  codeChaosOverlay.style("height", "100vh");
  codeChaosOverlay.style("display", "flex");
  codeChaosOverlay.style("justify-content", "center");
  codeChaosOverlay.style("align-items", "center");
  codeChaosOverlay.style("background-color", "black");
  codeChaosOverlay.style("z-index", "1500"); // Above everything else
  codeChaosOverlay.parent(document.body);

  // First show the digital rain GIF for 10 seconds
  let rainGif = createImg(
    "https://upload.wikimedia.org/wikipedia/commons/1/17/Digital_rain_animation_small_letters_clear.gif",
    "code chaos"
  );
  rainGif.style("max-width", "100%");
  rainGif.style("max-height", "100%");
  rainGif.style("width", "auto");
  rainGif.style("height", "auto");
  rainGif.parent(codeChaosOverlay);

  // After 10 seconds, swap to the static glitch image "forever"
  setTimeout(() => {
    if (!codeChaosOverlay) return;

    rainGif.remove();

    let finalImg = createImg(
      "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTpV9HGpnYT1I4tl1EYWcWQR-9qgx5VkFW0-g&s",
      "glitch forever"
    );
    finalImg.style("max-width", "100%");
    finalImg.style("max-height", "100%");
    finalImg.style("width", "auto");
    finalImg.style("height", "auto");
    finalImg.parent(codeChaosOverlay);
    // Stays forever until page reload
  }, 10000);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Run-Once Guard if (codeChaosOverlay) return;

Stops the sequence from being launched a second time while it's already running

conditional Delayed Image Swap Safety Check if (!codeChaosOverlay) return;

Makes sure the overlay still exists before swapping images, in case something removed it early

if (codeChaosOverlay) return;
Prevents launching a second overlay if chaos mode is toggled on again while one is already running.
codeChaosOverlay.style("z-index", "1500");
Uses an even higher stacking order than the Rickroll overlay so this always appears on top.
let rainGif = createImg( ... );
Displays a 'Matrix digital rain' GIF as the first stage of the chaos effect.
setTimeout(() => { ... }, 10000);
Waits 10 seconds before running the code that swaps the rain GIF for a permanent glitch image.
rainGif.remove();
Deletes the digital rain image element from the page.
let finalImg = createImg( ... );
Creates a new, permanent-looking glitch image that stays on screen until the page is refreshed.

generateResponse()

generateResponse() is the 'brain' of the chatbot: a chain of if-statements checks for specific keywords first (most predictable behavior), and only falls through to random-chance branches once no keyword matched, showing how rule-based bots layer certainty before randomness.

🔬 This gives a 1-in-5 chance of a meme image reply. What happens if you change 0.2 to 0.9 - does almost every normal reply turn into an image now?

  if (random() < 0.2) { // 0.2 = 20% = 1 in 5
    return {
      type: "image",
      src: random(potatoMemeImages)
    };
  }

🔬 This loop avoids repeating the last question, trying up to 10 times. What happens if you remove '&& tries < 10' entirely - could the loop ever get stuck if bobQuestions only had one item?

      do {
        questionToAsk = random(bobQuestions);
        tries++;
      } while (questionToAsk === lastQuestionAsked && tries < 10);
function generateResponse(message) {
  let lower = message.toLowerCase();

  // --- Potato AI says hi if user says hello or hi ---
  if (lower.includes("hello") || lower.includes("hi")) {
    return "Hi, I'm Potato AI 2.0 🥔";
  }

  // --- Funny Easter egg ---
  if (lower.includes("banana")) {
    return "WHY DID YOU SAY BANANA 🍌💀";
  }

  // --- Fries / wedges trigger (angry potato image) ---
  if (lower.includes("fries") || lower.includes("wedges")) {
    return {
      type: "image",
      src: "https://thumbs.dreamstime.com/b/angry-potato-illustration-vector-white-background-207022549.jpg"
    };
  }


  // --- If player says "67", Potato AI says "67" back ---
  if (lower === "67") {
    return "67";
  }

  // --- Display beef/steak image ---
  if (lower.includes("beef") || lower.includes("steak")) {
    return {
      type: "image",
      src: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSIYenM3G88GS03X0hV3CBUUM2GzJ2-ZTNA4g&s"
    };
  }

  // Detect coding/scripting requests
  if (
    lower.includes("code") ||
    lower.includes("script") ||
    lower.includes("program") ||
    lower.includes("function")
  ) {
    return "I'm just a humble potato, I can't code bro 🥔";
  }

  // Detect reading/summarizing/explaining requests or long text
  if (
    lower.includes("read") ||
    lower.includes("summarize") ||
    lower.includes("summary") ||
    lower.includes("explain") ||
    lower.includes("analyze") ||
    lower.length > 200 // long pasted paragraph
  ) {
    return "I ain't reading all that, spud.";
  }

  // ---- 1 in 5 chance to send one of the 3 potato meme images ----
  if (random() < 0.2) { // 0.2 = 20% = 1 in 5
    return {
      type: "image",
      src: random(potatoMemeImages)
    };
  }

  // Chance for Potato AI to ask a question (15% chance)
  if (random() < 0.15) {
    let questionToAsk;
    if (bobQuestions.length > 0) {
      let tries = 0;
      do {
        questionToAsk = random(bobQuestions);
        tries++;
      } while (questionToAsk === lastQuestionAsked && tries < 10);
      lastQuestionAsked = questionToAsk;
    } else {
      questionToAsk = "What's on your mind, fellow potato?";
    }
    return questionToAsk;
  }

  // --- Random off-script phrases (potato themed) ---
  let offScriptPhrases = [
    "... can you say that again bro I zoned out 👽",
    "I don't know, can you?",
    "What was the question again? My brain is mashed potatoes 🥔",
    "Oops, my peel is coming off, gotta go!",
    "Spud...",
    "Did you hear that? I think someone just opened a bag of chips.",
    "Is it snack time yet?"
  ];

  // Random chance for Potato AI to go off-script (25% chance)
  if (random() < 0.25) {
    let phrase = random(offScriptPhrases);
    return phrase;
  }

  // Final fallback depends on smart mode
  if (smartModeEnabled) {
    return "Hmm... let me think about that like a wise potato 🥔.";
  }

  return "I don't know ¯\\_(ツ)_/¯";
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Keyword Matching Chain if (lower.includes("hello") || lower.includes("hi")) { return "Hi, I'm Potato AI 2.0 🥔"; }

The first of many if-statements that check the lowercased message for specific trigger words before falling back to randomness

while-loop Avoid Repeating Questions do { questionToAsk = random(bobQuestions); tries++; } while (questionToAsk === lastQuestionAsked && tries < 10);

Keeps picking a random question (up to 10 tries) until it finds one that's different from the last question asked

conditional Smart Mode Fallback if (smartModeEnabled) { return "Hmm... let me think about that like a wise potato 🥔."; }

Chooses a fancier-sounding fallback line if smart mode is on, otherwise uses a plain shrug

let lower = message.toLowerCase();
Converts the user's message to all-lowercase so keyword checks work no matter how they capitalized it.
if (lower.includes("hello") || lower.includes("hi")) {
Checks if the message contains either 'hello' or 'hi' anywhere in it.
if (lower === "67") {
Uses strict equality (not includes) so this only fires if the ENTIRE message is exactly '67'.
lower.length > 200 // long pasted paragraph
Treats any very long message as a 'wall of text' the potato refuses to read, regardless of its content.
if (random() < 0.2) { // 0.2 = 20% = 1 in 5
Once none of the keyword rules matched, there's a 20% random chance to reply with a meme image instead of text.
src: random(potatoMemeImages)
p5's random() function, when given an array, picks and returns one random element from it - here, one of the three meme image URLs.
do { questionToAsk = random(bobQuestions); tries++; } while (questionToAsk === lastQuestionAsked && tries < 10);
Repeatedly rolls a random question until it gets one different from last time (or gives up after 10 tries), so the bot doesn't ask the exact same question twice in a row.
if (random() < 0.25) {
A 25% chance to pick a random silly off-script phrase instead of a normal answer.
return "I don't know ¯\\_(ツ)_/¯";
The final fallback reply if absolutely nothing else matched or triggered - a plain shrug emoticon.

addMessage()

addMessage() shows how a single 'render' function can branch based on the type of data it receives (a string vs. an object with a .type field), a common pattern for handling multiple kinds of content with one function.

🔬 This recursive function is never actually called from generateResponse() anywhere - it's dead code! What happens if you make generateResponse() sometimes return {type: 'repeatingImage', src: potatoMemeImages[0], count: 3}?

    function addRepeatingImage() {
      if (currentCount < totalCount) {
        // Recursively call addMessage to display a single image bubble
        addMessage({ type: "image", src: imageUrl }, "ai");
        currentCount++;
        setTimeout(addRepeatingImage, 750); // Delay before adding the next (0.75 seconds)
      }
    }
function addMessage(content, sender) {
  // Handle the special "darkScreen" event
  if (typeof content === "object" && content.type === "darkScreen") {
    document.body.style.backgroundColor = "black";

    // Hide the entire chat interface (main flex container)
    mainContainer.style("display", "none");

    // Create a temporary container for the GIF directly in the body
    let tempGifContainer = createDiv();
    tempGifContainer.style("position", "fixed");
    tempGifContainer.style("top", "0");
    tempGifContainer.style("left", "0");
    tempGifContainer.style("width", "100vw");
    tempGifContainer.style("height", "100vh");
    tempGifContainer.style("display", "flex");
    tempGifContainer.style("justify-content", "center");
    tempGifContainer.style("align-items", "center");
    tempGifContainer.style("z-index", "999"); // Below exit button, but above everything else
    tempGifContainer.parent(document.body);

    let gif = createImg(content.gifSrc, "Potatoes Dancing");
    gif.style("max-width", "90%");
    gif.style("max-height", "90%");
    gif.style("width", "auto");
    gif.style("height", "auto");
    gif.parent(tempGifContainer); // correct container

    // Remove the keypress listener so Enter key doesn't work
    inputBox.elt.removeEventListener("keypress", keypressListener);

    return; // Stop further processing for this message
  }

  // Handle repeating image trigger
  if (typeof content === "object" && content.type === "repeatingImage") {
    const imageUrl = content.src;
    const totalCount = content.count;
    let currentCount = 0;

    function addRepeatingImage() {
      if (currentCount < totalCount) {
        // Recursively call addMessage to display a single image bubble
        addMessage({ type: "image", src: imageUrl }, "ai");
        currentCount++;
        setTimeout(addRepeatingImage, 750); // Delay before adding the next (0.75 seconds)
      }
    }
    addRepeatingImage(); // Start the first one
    return; // Stop further processing for this message
  }

  // --- Regular message/image handling ---

  // If chaos mode is on, "mess with" Potato AI's text output
  if (sender === "ai" && codeChaosEnabled && typeof content === "string") {
    content = glitchText(content);
  }

  let bubble = createDiv(); // Create the bubble container
  bubble.style("padding", "10px");
  bubble.style("margin", "5px 0");
  bubble.style("border-radius", "15px");
  bubble.style("max-width", "60%");
  bubble.style("word-wrap", "break-word"); // Ensure text wraps
  bubble.style("border", "1px solid #d2b48c");

  // Determine sender styling
  if (sender === "user") {
    bubble.style("background-color", "#ffe4b5"); // light potato-ish color
    bubble.style("align-self", "flex-end");
  } else { // 'ai' messages
    bubble.style("background-color", "#fffaf0");
    bubble.style("align-self", "flex-start");
  }

  // Prepend AI's name if the sender is 'ai'
  let prefix = "";
  if (sender === "ai") {
    // larry is the AI name now
    prefix = "larry: ";
  }

  // Check if content is an image object or text
  if (typeof content === "object" && content.type === "image") {
    // If there's a prefix (name), add it as a separate text node
    if (prefix) {
      let prefixSpan = createSpan(prefix);
      prefixSpan.parent(bubble);
    }

    let img = createImg(content.src, "meme image"); // Create p5.js image element
    img.style("max-width", "100%"); // Ensure image fits within the bubble
    img.style("height", "auto"); // Maintain aspect ratio
    img.style("display", "block"); // Remove extra space below image
    img.style("margin-top", "5px"); // Space between prefix and image
    img.parent(bubble); // Add the image to the bubble
  } else {
    // It's text content
    bubble.html(prefix + content); // Set the HTML content directly
  }

  bubble.parent(chatContainer);
  // Scroll the newly added message bubble into view with smooth animation
  bubble.elt.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Dark Screen Event Handler if (typeof content === "object" && content.type === "darkScreen") { ... }

Special-cases the rare dark screen event: hides the whole chat UI and shows a fullscreen GIF instead

conditional Repeating Image Handler (unused) function addRepeatingImage() { if (currentCount < totalCount) { ... } }

Recursively posts the same image multiple times with a delay between each - fully built but never actually triggered anywhere else in the code

conditional Apply Glitch Text if (sender === "ai" && codeChaosEnabled && typeof content === "string") { content = glitchText(content); }

Runs AI text replies through glitchText() whenever chaos mode is turned on

if (typeof content === "object" && content.type === "darkScreen") {
Checks if this message is the special dark-screen event object rather than normal text or an image.
mainContainer.style("display", "none");
Hides the whole chat UI so only the fullscreen GIF is visible.
function addRepeatingImage() { if (currentCount < totalCount) { ... } }
Defines a small recursive helper that posts one image, then schedules itself again after a delay, until it has posted totalCount images.
if (sender === "ai" && codeChaosEnabled && typeof content === "string") { content = glitchText(content); }
Only mangles AI text replies (not user messages or images) and only when chaos mode is switched on.
let bubble = createDiv();
Creates the div that will visually represent one chat message.
if (sender === "user") { bubble.style("background-color", "#ffe4b5"); bubble.style("align-self", "flex-end"); }
Gives user messages a distinct color and aligns them to the right side of the chat.
let prefix = ""; if (sender === "ai") { prefix = "larry: "; }
Only AI messages get the 'larry: ' name prefix added in front of them.
if (typeof content === "object" && content.type === "image") { ... } else { bubble.html(prefix + content); }
Decides whether to build an <img> element inside the bubble or just set the bubble's HTML text, depending on what kind of content was passed in.
bubble.elt.scrollIntoView({ behavior: 'smooth', block: 'end' });
Smoothly scrolls the chat so the newest bubble is always visible without a jarring jump.

glitchText()

glitchText() is a simple text-corruption effect built entirely from a for-loop and a couple of random() checks per character - a pattern you can reuse anytime you want to add 'noise' to strings, like glitch subtitles or corrupted terminal output.

🔬 This flips case on 30% of characters. What happens if you crank 0.3 up to 0.95 - does the text become almost entirely random-cased, and does that feel more or less 'glitchy' than the original?

    // Randomize case sometimes
    if (random() < 0.3) {
      ch = random([ch.toLowerCase(), ch.toUpperCase()]);
    }
function glitchText(str) {
  let result = "";
  for (let i = 0; i < str.length; i++) {
    let ch = str[i];

    // Randomize case sometimes
    if (random() < 0.3) {
      ch = random([ch.toLowerCase(), ch.toUpperCase()]);
    }

    result += ch;

    // Occasionally repeat a character
    if (random() < 0.1) {
      result += ch;
    }
  }

  // Occasionally add a little "glitch" suffix
  if (random() < 0.4) {
    result += " <glitch>";
  }
  return result;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Per-Character Glitch Loop for (let i = 0; i < str.length; i++) { ... }

Walks through every single character of the input string one at a time

conditional Random Case Flip if (random() < 0.3) { ch = random([ch.toLowerCase(), ch.toUpperCase()]); }

30% chance per character to randomly force it to lowercase or uppercase

conditional Random Character Repeat if (random() < 0.1) { result += ch; }

10% chance per character to duplicate it, creating a stutter/glitch look

let result = "";
Starts with an empty string that characters get appended to one at a time.
for (let i = 0; i < str.length; i++) {
Loops once for every character in the original text.
let ch = str[i];
Grabs the current character at position i.
if (random() < 0.3) { ch = random([ch.toLowerCase(), ch.toUpperCase()]); }
30% of the time, randomly picks either the lowercase or uppercase version of this character.
result += ch;
Adds the (possibly case-flipped) character onto the growing result string.
if (random() < 0.1) { result += ch; }
10% of the time, appends the same character a second time, creating a stuttering glitch effect.
if (random() < 0.4) { result += " <glitch>"; }
After processing the whole string, there's a 40% chance to tack on a literal ' <glitch>' tag at the very end.
return result;
Sends the fully glitched string back to whatever called glitchText().

📦 Key Variables

inputBox object

Reference to the p5 DOM input element where the user types their message.

let inputBox;
sendButton object

Reference to the Send button element, used to attach the click handler.

let sendButton;
chatContainer object

The scrollable div that holds every chat bubble (user and AI messages).

let chatContainer;
thinkingCowImageElement object

Temporary reference to the 'thinking' image shown while waiting for a reply, so it can be removed later.

let thinkingCowImageElement;
mainContainer object

The outer flex container that holds the title, controls, chat window, and input area.

let mainContainer;
titleElement object

The <h1> heading element showing the sketch's title.

let titleElement;
subtitleElement object

The small copyright/subtitle paragraph under the title.

let subtitleElement;
inputArea object

The div wrapping the text input and Send button at the bottom of the page.

let inputArea;
keypressListener object

Stores the Enter-key event handler function so it can be removed later (e.g. during the Rickroll).

let keypressListener;
settingsButton object

The button that opens and closes the settings panel.

let settingsButton;
settingsPanel object

The floating panel containing smart mode, background color, and the chaos button.

let settingsPanel;
smartModeCheckbox object

The checkbox UI element used to toggle smart mode on and off.

let smartModeCheckbox;
bgColorPicker object

The color picker input used to change the page and chat background color live.

let bgColorPicker;
smartModeEnabled boolean

Tracks whether smart mode is on, which changes the bot's final fallback reply.

let smartModeEnabled = false;
settingsVisible boolean

Tracks whether the settings panel is currently shown or hidden.

let settingsVisible = false;
playGifButton object

The 'Play Potato GIF' button that triggers the escalating 'are you sure' joke.

let playGifButton;
playGifClickCount number

Counts how many times the Play GIF button has been confirmed, used to build the escalating 'sure' phrase.

let playGifClickCount = 0;
rickrollOverlay object

Reference to the fullscreen Rickroll overlay div, used as a guard so it's only created once.

let rickrollOverlay = null;
codeChaosEnabled boolean

Tracks whether 'chaos mode' is active, which makes AI text replies pass through glitchText().

let codeChaosEnabled = false;
codeChaosButton object

The Settings panel button that toggles chaos mode on and off.

let codeChaosButton;
codeChaosOverlay object

Reference to the fullscreen Matrix-style chaos overlay div, used as a guard so it only plays once.

let codeChaosOverlay = null;
bobQuestions array

A list of canned follow-up questions the bot can randomly ask the user.

let bobQuestions = ["So, what's your favorite thing to do for fun?", ...];
potatoMemeImages array

A list of image URLs the bot can randomly pick from when replying with a meme image.

let potatoMemeImages = ["https://...", "https://...", "https://..."];
lastQuestionAsked string

Remembers the most recently asked question so the bot tries not to repeat itself back-to-back.

let lastQuestionAsked = "";

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG addMessage() - repeatingImage handler

The {type: 'repeatingImage', src, count} content path is fully implemented (with a recursive setTimeout loop) but nothing in sendMessage() or generateResponse() ever returns that shape, so this entire branch is dead code that can never execute.

💡 Either have generateResponse() occasionally return a repeatingImage object to actually use this feature, or remove the unused branch to simplify addMessage().

PERFORMANCE addMessage()

Every user and AI message permanently adds a new div to chatContainer with no limit, so a long conversation keeps growing the DOM forever, which can eventually slow down scrolling and rendering.

💡 Cap the number of stored bubbles by removing the oldest child once chatContainer.childElementCount passes a threshold (e.g. 100), or add a 'Clear chat' button.

STYLE sendMessage() / global variables

The variable thinkingCowImageElement still references an old 'cow' concept even though the sketch and image are now potato-themed, which is confusing for anyone reading the code.

💡 Rename it to thinkingPotatoImageElement (and update comments) so the code matches the current theme.

BUG showRickrollOverlay()

Only the Enter-key listener on the input box is removed when the Rickroll overlay appears; sendButton.mousePressed(sendMessage) is never disabled, so clicking Send can still add hidden chat bubbles behind the overlay.

💡 Also disable the Send button (e.g. sendButton.elt.disabled = true;) inside showRickrollOverlay() so no further messages can be sent once the user is rickrolled.

🔄 Code Flow

Code flow showing setup, sendmessage, handleplaygifbutton, showrickrolloverlay, showcodechaossequence, generateresponse, addmessage, glitchtext

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> smartmode[smart-mode-toggle] draw --> chaos[chaos-toggle] draw --> settings[settings-toggle] draw --> empty[empty-check] draw --> thinking[thinking-cleanup] draw --> darkscreen[darkscreen-roll] draw --> send[sendmessage] send --> empty send --> thinking thinking --> thinkingcleanup[thinking-cleanup] thinkingcleanup --> generateresponse[generateresponse] generateresponse --> keyword[keyword-checks] keyword --> fallback[fallback] keyword --> question[question-loop] question --> empty question --> questionloop[question-loop] questionloop --> generateresponse generateresponse --> addmsg[addmessage] addmsg --> glitch[chaos-glitch] glitch --> glitchtext[glitchtext] glitchtext --> addmsg addmsg --> draw draw --> playgif[handleplaygifbutton] playgif --> firstclick[first-click] firstclick --> sure[sure-repeat] sure --> confirm[confirm-check] confirm --> guard[guard] confirm --> guard2[guard2] guard2 --> showgif[showcodechaossequence] showgif --> delayed[delayed-swap] delayed --> draw draw --> rickroll[showrickrolloverlay] rickroll --> guard click setup href "#fn-setup" click draw href "#fn-draw" click smartmode href "#sub-smart-mode-toggle" click chaos href "#sub-chaos-toggle" click settings href "#sub-settings-toggle" click empty href "#sub-empty-check" click thinking href "#sub-thinking-cleanup" click darkscreen href "#sub-darkscreen-roll" click send href "#fn-sendmessage" click thinkingcleanup href "#sub-thinking-cleanup" click generateresponse href "#fn-generateresponse" click keyword href "#sub-keyword-checks" click fallback href "#sub-fallback" click question href "#sub-question-loop" click questionloop href "#sub-question-loop" click addmsg href "#fn-addmessage" click glitch href "#sub-chaos-glitch" click glitchtext href "#fn-glitchtext" click playgif href "#fn-handleplaygifbutton" click firstclick href "#sub-first-click" click sure href "#sub-sure-repeat" click confirm href "#sub-confirm-check" click guard href "#sub-guard" click guard2 href "#sub-guard2" click showgif href "#fn-showcodechaossequence" click delayed href "#sub-delayed-swap" click rickroll href "#fn-showrickrolloverlay"

❓ Frequently Asked Questions

What visual elements can I expect from the Potato AI 2.0 sketch?

The sketch features a playful UI with interactive elements, including a main container, title, and potentially humorous potato-themed images.

How can users engage with the Potato AI 2.0 application?

Users can interact by typing in an input box, clicking a send button to receive responses, and even experimenting with settings like smart mode and code chaos.

What creative coding concepts are showcased in Potato AI 2.0?

This sketch demonstrates the use of dynamic UI components and randomness in generating questions and responses, enhancing user engagement through a playful chatbot experience.

Preview

potato ai 2.0 OFFICAL - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of potato ai 2.0 OFFICAL - Code flow showing setup, sendmessage, handleplaygifbutton, showrickrolloverlay, showcodechaossequence, generateresponse, addmessage, glitchtext
Code Flow Diagram