diff --git a/src/addons/addons.js b/src/addons/addons.js
index c58158695..7c8148bc2 100644
--- a/src/addons/addons.js
+++ b/src/addons/addons.js
@@ -81,12 +81,14 @@ const addons = [
'tw-disable-cloud-variables',
'tw-disable-compiler',
'editor-stepping',
- 'qcode'
+ 'qcode',
+ 'boxy-assistant'
];
const newAddons = [
'expanded-backpack',
- 'qcode'
+ 'qcode',
+ 'boxy-assistant'
];
// eslint-disable-next-line import/no-commonjs
diff --git a/src/addons/addons/boxy-assistant/_manifest_entry.js b/src/addons/addons/boxy-assistant/_manifest_entry.js
new file mode 100644
index 000000000..b1c6d7bb2
--- /dev/null
+++ b/src/addons/addons/boxy-assistant/_manifest_entry.js
@@ -0,0 +1,33 @@
+/* OmniBlocks custom addon */
+const manifest = {
+ "editorOnly": true,
+ "noTranslations": true,
+ "name": "Boxy AI Assistant",
+ "description": "An AI-powered assistant in the form of Boxy to help you learn and code better. Features local AI models and helpful animations.",
+ "credits": [
+ {
+ "name": "supervoidcoder",
+ "link": "https://github.com/supervoidcoder"
+ },
+ {
+ "name": "OmniBlocks Team"
+ }
+ ],
+ "dynamicDisable": true,
+ "userscripts": [
+ {
+ "url": "userscript.js"
+ }
+ ],
+ "userstyles": [
+ {
+ "url": "style.css"
+ }
+ ],
+ "tags": [
+ "featured",
+ "new"
+ ],
+ "enabledByDefault": false
+};
+export default manifest;
diff --git a/src/addons/addons/boxy-assistant/_runtime_entry.js b/src/addons/addons/boxy-assistant/_runtime_entry.js
new file mode 100644
index 000000000..f8ac5750a
--- /dev/null
+++ b/src/addons/addons/boxy-assistant/_runtime_entry.js
@@ -0,0 +1,7 @@
+/* OmniBlocks custom addon */
+import _js from "./userscript.js";
+import _css from "!css-loader!./style.css";
+export const resources = {
+ "userscript.js": _js,
+ "style.css": _css,
+};
diff --git a/src/addons/addons/boxy-assistant/style.css b/src/addons/addons/boxy-assistant/style.css
new file mode 100644
index 000000000..c5c1db7f0
--- /dev/null
+++ b/src/addons/addons/boxy-assistant/style.css
@@ -0,0 +1,355 @@
+/* Boxy AI Assistant Styles */
+
+/* Container that overlays the entire editor */
+.boxy-assistant-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ pointer-events: none;
+ z-index: 9999;
+ overflow: hidden;
+}
+
+/* Boxy character */
+.boxy-character {
+ position: absolute;
+ width: 150px;
+ height: 156px;
+ pointer-events: auto;
+ cursor: pointer;
+ transform-origin: center center;
+ user-select: none;
+}
+
+.boxy-character.dragging {
+ cursor: grabbing;
+}
+
+.boxy-character:hover {
+ filter: drop-shadow(0 0 10px rgba(0, 186, 135, 0.5));
+}
+
+.boxy-character:focus {
+ outline: 3px solid #00ba87;
+ outline-offset: 5px;
+}
+
+.boxy-svg-wrapper {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.boxy-svg-wrapper svg {
+ width: 100%;
+ height: auto;
+}
+
+/* Text bubble */
+.boxy-text-bubble {
+ position: fixed;
+ max-width: 300px;
+ min-width: 200px;
+ background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%);
+ border-radius: 20px;
+ padding: 15px 20px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+ pointer-events: auto;
+ animation: bubbleAppear 0.3s ease;
+ z-index: 10000;
+}
+
+@keyframes bubbleAppear {
+ from {
+ opacity: 0;
+ transform: scale(0.8) translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.boxy-text-bubble::after {
+ content: "";
+ position: absolute;
+ bottom: -10px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 15px solid transparent;
+ border-right: 15px solid transparent;
+ border-top: 15px solid #00ba87;
+}
+
+.boxy-bubble-content {
+ color: white;
+ font-family: "Helvetica Neue", Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.boxy-message {
+ margin: 0;
+ font-weight: 500;
+}
+
+/* Chat Interface */
+.boxy-chat-interface {
+ position: fixed;
+ width: 400px;
+ max-width: calc(100vw - 40px);
+ height: 500px;
+ max-height: calc(100vh - 40px);
+ background: white;
+ border-radius: 15px;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
+ pointer-events: auto;
+ z-index: 10001;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.boxy-chat-header {
+ background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%);
+ color: white;
+ padding: 15px 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.boxy-chat-header h3 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.boxy-chat-close {
+ background: none;
+ border: none;
+ color: white;
+ font-size: 28px;
+ cursor: pointer;
+ padding: 0;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: background 0.2s;
+}
+
+.boxy-chat-close:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.boxy-chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 15px;
+ background: #f5f5f5;
+}
+
+.boxy-chat-message {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+ align-items: flex-start;
+}
+
+.boxy-message-bot {
+ flex-direction: row;
+}
+
+.boxy-message-user {
+ flex-direction: row-reverse;
+}
+
+.boxy-chat-avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ flex-shrink: 0;
+}
+
+.boxy-message-user .boxy-chat-avatar {
+ background: #888;
+}
+
+.boxy-chat-text {
+ background: white;
+ padding: 10px 15px;
+ border-radius: 15px;
+ max-width: 70%;
+ word-wrap: break-word;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.boxy-message-user .boxy-chat-text {
+ background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%);
+ color: white;
+}
+
+.boxy-thinking .boxy-chat-text {
+ opacity: 0.7;
+ font-style: italic;
+}
+
+.boxy-chat-input-area {
+ padding: 15px;
+ background: white;
+ border-top: 1px solid #ddd;
+ display: flex;
+ gap: 10px;
+}
+
+.boxy-chat-input {
+ flex: 1;
+ padding: 10px 15px;
+ border: 2px solid #ddd;
+ border-radius: 25px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.boxy-chat-input:focus {
+ border-color: #00ba87;
+}
+
+.boxy-chat-send {
+ padding: 10px 20px;
+ background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%);
+ color: white;
+ border: none;
+ border-radius: 25px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.boxy-chat-send:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 186, 135, 0.4);
+}
+
+.boxy-chat-send:active {
+ transform: translateY(0);
+}
+
+.boxy-api-key-setup {
+ padding: 15px;
+ background: #fff8e1;
+ border-top: 1px solid #ffe082;
+}
+
+.boxy-api-key-setup p {
+ margin: 0 0 10px 0;
+ font-size: 13px;
+}
+
+.boxy-api-key-setup strong {
+ color: #f57c00;
+}
+
+.boxy-api-key-setup a {
+ color: #0067bb;
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.boxy-api-key-setup a:hover {
+ text-decoration: underline;
+}
+
+.boxy-api-key-input {
+ width: 100%;
+ padding: 10px;
+ border: 2px solid #ffe082;
+ border-radius: 8px;
+ font-size: 14px;
+ margin-bottom: 10px;
+ box-sizing: border-box;
+}
+
+.boxy-api-key-save {
+ width: 100%;
+ padding: 10px;
+ background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.boxy-api-key-save:hover {
+ transform: translateY(-2px);
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .boxy-character {
+ width: 100px;
+ height: 104px;
+ }
+
+ .boxy-text-bubble {
+ max-width: 200px;
+ font-size: 12px;
+ padding: 10px 15px;
+ }
+
+ .boxy-chat-interface {
+ width: calc(100vw - 20px);
+ height: calc(100vh - 20px);
+ left: 10px !important;
+ top: 10px !important;
+ }
+}
+
+/* Dark mode support */
+@media (prefers-color-scheme: dark) {
+ .boxy-text-bubble {
+ box-shadow: 0 4px 20px rgba(255, 255, 255, 0.1);
+ }
+
+ .boxy-chat-interface {
+ background: #2a2a2a;
+ color: #fff;
+ }
+
+ .boxy-chat-messages {
+ background: #1a1a1a;
+ }
+
+ .boxy-chat-text {
+ background: #3a3a3a;
+ color: #fff;
+ }
+
+ .boxy-chat-input-area {
+ background: #2a2a2a;
+ border-top-color: #444;
+ }
+
+ .boxy-chat-input {
+ background: #3a3a3a;
+ color: #fff;
+ border-color: #555;
+ }
+}
diff --git a/src/addons/addons/boxy-assistant/userscript.js b/src/addons/addons/boxy-assistant/userscript.js
new file mode 100644
index 000000000..d8eb7f84d
--- /dev/null
+++ b/src/addons/addons/boxy-assistant/userscript.js
@@ -0,0 +1,479 @@
+/* Boxy AI Assistant - Main Userscript */
+
+export default async function ({ addon, console }) {
+ console.log("Boxy AI Assistant initializing...");
+
+ // Create the Boxy container that overlays the entire editor
+ const boxyContainer = document.createElement("div");
+ boxyContainer.className = "boxy-assistant-container";
+ boxyContainer.setAttribute("role", "complementary");
+ boxyContainer.setAttribute("aria-label", "Boxy AI Assistant - Your coding helper");
+ addon.tab.displayNoneWhileDisabled(boxyContainer, { display: "block" });
+
+ // Create the Boxy character element
+ const boxyCharacter = document.createElement("div");
+ boxyCharacter.className = "boxy-character";
+ boxyCharacter.setAttribute("role", "button");
+ boxyCharacter.setAttribute("aria-label", "Boxy - Click to chat, or drag to move");
+ boxyCharacter.setAttribute("tabindex", "0");
+ boxyCharacter.innerHTML = `
+
+
+
+ `;
+
+ // Create text bubble for Boxy's messages
+ const textBubble = document.createElement("div");
+ textBubble.className = "boxy-text-bubble";
+ textBubble.style.display = "none";
+ textBubble.setAttribute("role", "status");
+ textBubble.setAttribute("aria-live", "polite");
+ textBubble.innerHTML = `
+
+
Hi! I'm Boxy, your AI assistant!
+
+ `;
+
+ // Create chat interface
+ const chatInterface = document.createElement("div");
+ chatInterface.className = "boxy-chat-interface";
+ chatInterface.style.display = "none";
+ chatInterface.setAttribute("role", "dialog");
+ chatInterface.setAttribute("aria-label", "Chat with Boxy");
+ chatInterface.innerHTML = `
+
+
+
+
🤖
+
Hi! I'm Boxy, your AI assistant! I can help you learn to code. To get started, you'll need to provide an API key from Pollinations AI.
+
+
+
+
+
+
+
+
⚠️ API Key Required
+
Boxy uses Pollinations AI (BYOK - Bring Your Own Key). Get your free key at enter.pollinations.ai
+
+
+
+ `;
+
+ // Assemble the components
+ boxyContainer.appendChild(boxyCharacter);
+ boxyContainer.appendChild(textBubble);
+ boxyContainer.appendChild(chatInterface);
+
+ // State variables
+ let xOffset = 0;
+ let yOffset = 0;
+ let boxyHideTimeoutId = null;
+ let apiKey = localStorage.getItem('boxy-api-key') || '';
+ let chatHistory = [];
+
+ // Wait for the editor to be ready
+ while (true) {
+ await addon.tab.waitForElement('[class*="stage-wrapper"]', {
+ markAsSeen: true,
+ reduxEvents: ["scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED"],
+ });
+
+ if (addon.tab.editorMode === "editor") {
+ document.body.appendChild(boxyContainer);
+ console.log("Boxy added to editor!");
+
+ initializeBoxyPosition();
+ makeBoxyDraggable();
+ setupChatInterface();
+
+ setTimeout(() => {
+ showBoxyMessage("Hi! I'm Boxy! Click me to chat, or drag me to move!");
+ }, 1000);
+
+ break;
+ }
+ }
+
+ // Initialize Boxy's starting position
+ function initializeBoxyPosition() {
+ boxyCharacter.style.left = "calc(100vw - 200px)";
+ boxyCharacter.style.top = "calc(100vh - 300px)";
+ }
+
+ // Update text bubble position relative to Boxy
+ function updateBubblePosition() {
+ const boxyRect = boxyCharacter.getBoundingClientRect();
+ textBubble.style.left = `${boxyRect.left}px`;
+ textBubble.style.top = `${boxyRect.top - textBubble.offsetHeight - 20}px`;
+ }
+
+ // Make Boxy draggable
+ function makeBoxyDraggable() {
+ let isDragging = false;
+ let dragMoved = false;
+ let currentX = 0;
+ let currentY = 0;
+ let initialX = 0;
+ let initialY = 0;
+
+ function dragStart(e) {
+ if (e.type === "mousedown") {
+ initialX = e.clientX - xOffset;
+ initialY = e.clientY - yOffset;
+ } else if (e.type === "touchstart") {
+ initialX = e.touches[0].clientX - xOffset;
+ initialY = e.touches[0].clientY - yOffset;
+ }
+
+ if (e.target === boxyCharacter || boxyCharacter.contains(e.target)) {
+ isDragging = true;
+ dragMoved = false;
+ boxyCharacter.classList.add("dragging");
+
+ // Attach drag listeners only when dragging starts
+ document.addEventListener("mousemove", drag);
+ document.addEventListener("mouseup", dragEnd);
+ document.addEventListener("touchmove", drag);
+ document.addEventListener("touchend", dragEnd);
+ }
+ }
+
+ function drag(e) {
+ if (isDragging) {
+ e.preventDefault();
+ dragMoved = true;
+
+ if (e.type === "mousemove") {
+ currentX = e.clientX - initialX;
+ currentY = e.clientY - initialY;
+ } else if (e.type === "touchmove") {
+ currentX = e.touches[0].clientX - initialX;
+ currentY = e.touches[0].clientY - initialY;
+ }
+
+ xOffset = currentX;
+ yOffset = currentY;
+
+ setBoxyTranslate(currentX, currentY);
+ updateBubblePosition();
+ }
+ }
+
+ function dragEnd(e) {
+ if (isDragging) {
+ // Only update if we actually computed positions
+ if (typeof currentX === "number" && typeof currentY === "number") {
+ initialX = currentX;
+ initialY = currentY;
+ }
+
+ isDragging = false;
+ boxyCharacter.classList.remove("dragging");
+
+ // Remove drag listeners
+ document.removeEventListener("mousemove", drag);
+ document.removeEventListener("mouseup", dragEnd);
+ document.removeEventListener("touchmove", drag);
+ document.removeEventListener("touchend", dragEnd);
+
+ // If we didn't move much, treat it as a click
+ if (!dragMoved) {
+ toggleChat();
+ }
+ }
+ }
+
+ function setBoxyTranslate(xPos, yPos) {
+ boxyCharacter.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
+ }
+
+ // Keyboard support for accessibility
+ function handleKeyDown(e) {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ toggleChat();
+ } else if (e.key.startsWith("Arrow")) {
+ e.preventDefault();
+ const step = e.shiftKey ? 20 : 5;
+ switch (e.key) {
+ case "ArrowLeft":
+ xOffset -= step;
+ break;
+ case "ArrowRight":
+ xOffset += step;
+ break;
+ case "ArrowUp":
+ yOffset -= step;
+ break;
+ case "ArrowDown":
+ yOffset += step;
+ break;
+ }
+ currentX = xOffset;
+ currentY = yOffset;
+ setBoxyTranslate(xOffset, yOffset);
+ updateBubblePosition();
+ }
+ }
+
+ boxyCharacter.addEventListener("mousedown", dragStart);
+ boxyCharacter.addEventListener("touchstart", dragStart);
+ boxyCharacter.addEventListener("keydown", handleKeyDown);
+
+ // Cleanup function
+ const cleanup = () => {
+ boxyCharacter.removeEventListener("mousedown", dragStart);
+ boxyCharacter.removeEventListener("touchstart", dragStart);
+ boxyCharacter.removeEventListener("keydown", handleKeyDown);
+ document.removeEventListener("mousemove", drag);
+ document.removeEventListener("mouseup", dragEnd);
+ document.removeEventListener("touchmove", drag);
+ document.removeEventListener("touchend", dragEnd);
+ };
+
+ return cleanup;
+ }
+
+ // Show a message in Boxy's text bubble
+ function showBoxyMessage(message, duration = 5000) {
+ const messageElement = textBubble.querySelector(".boxy-message");
+ messageElement.textContent = message;
+ textBubble.style.display = "block";
+ updateBubblePosition();
+
+ // Clear any existing timeout
+ if (boxyHideTimeoutId !== null) {
+ clearTimeout(boxyHideTimeoutId);
+ boxyHideTimeoutId = null;
+ }
+
+ // Auto-hide after duration
+ if (duration > 0) {
+ boxyHideTimeoutId = setTimeout(() => {
+ textBubble.style.display = "none";
+ boxyHideTimeoutId = null;
+ }, duration);
+ }
+ }
+
+ // Toggle chat interface
+ function toggleChat() {
+ const isVisible = chatInterface.style.display !== "none";
+ chatInterface.style.display = isVisible ? "none" : "block";
+
+ if (!isVisible) {
+ // Position chat near Boxy
+ const boxyRect = boxyCharacter.getBoundingClientRect();
+ chatInterface.style.left = `${Math.max(20, boxyRect.left - 320)}px`;
+ chatInterface.style.top = `${Math.max(20, boxyRect.top)}px`;
+
+ // Focus input
+ const input = chatInterface.querySelector(".boxy-chat-input");
+ if (apiKey) {
+ input.focus();
+ }
+ }
+ }
+
+ // Setup chat interface handlers
+ function setupChatInterface() {
+ const closeBtn = chatInterface.querySelector(".boxy-chat-close");
+ const sendBtn = chatInterface.querySelector(".boxy-chat-send");
+ const input = chatInterface.querySelector(".boxy-chat-input");
+ const apiKeyInput = chatInterface.querySelector(".boxy-api-key-input");
+ const apiKeySaveBtn = chatInterface.querySelector(".boxy-api-key-save");
+ const messagesContainer = chatInterface.querySelector(".boxy-chat-messages");
+ const apiKeySetup = chatInterface.querySelector(".boxy-api-key-setup");
+
+ // Load saved API key
+ if (apiKey) {
+ apiKeySetup.style.display = "none";
+ apiKeyInput.value = apiKey;
+ }
+
+ closeBtn.addEventListener("click", () => {
+ chatInterface.style.display = "none";
+ });
+
+ apiKeySaveBtn.addEventListener("click", () => {
+ const key = apiKeyInput.value.trim();
+ if (key) {
+ apiKey = key;
+ localStorage.setItem('boxy-api-key', key);
+ apiKeySetup.style.display = "none";
+ addChatMessage("API key saved! You can now chat with me.", "bot");
+ input.focus();
+ } else {
+ alert("Please enter a valid API key");
+ }
+ });
+
+ async function sendMessage() {
+ const message = input.value.trim();
+ if (!message) return;
+
+ if (!apiKey) {
+ addChatMessage("Please set up your API key first!", "bot");
+ return;
+ }
+
+ // Add user message
+ addChatMessage(message, "user");
+ input.value = "";
+
+ // Show thinking state
+ addChatMessage("...", "bot", true);
+
+ try {
+ // Build system prompt with Boxy's personality and guidelines
+ const systemPrompt = `You are Boxy, a friendly AI assistant designed to help kids learn coding in OmniBlocks (a Scratch-based platform).
+
+Your role:
+- Help users understand coding concepts
+- Suggest approaches and brainstorm ideas
+- Guide users through problem-solving
+- Explain how blocks and features work
+- Encourage learning and experimentation
+
+Important rules:
+- NEVER write complete code for users - they must learn by doing
+- If asked to "write code" or "make a program", politely refuse and offer to teach them how instead
+- Use simple, kid-friendly language
+- Be encouraging and supportive
+- Keep responses concise and focused
+
+Your catchphrase when refusing to write code: "Sorry, buddy. I can suggest things to you, help you learn a concept, or brainstorm fun things, but if you're here to vibe code, this ain't the place for you, pal."
+
+User's question: ${message}`;
+
+ // Call Pollinations AI API with system prompt
+ const response = await fetch(`https://text.pollinations.ai/prompt/${encodeURIComponent(systemPrompt)}`, {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`
+ }
+ });
+
+ // Remove thinking message
+ const thinkingMsg = messagesContainer.querySelector(".boxy-thinking");
+ if (thinkingMsg) thinkingMsg.remove();
+
+ if (!response.ok) {
+ throw new Error(`API Error: ${response.status}`);
+ }
+
+ const aiResponse = await response.text();
+ addChatMessage(aiResponse, "bot");
+
+ // Add to history
+ chatHistory.push({ role: "user", content: message });
+ chatHistory.push({ role: "assistant", content: aiResponse });
+
+ } catch (error) {
+ console.error("Chat error:", error);
+ const thinkingMsg = messagesContainer.querySelector(".boxy-thinking");
+ if (thinkingMsg) thinkingMsg.remove();
+ addChatMessage(`Oops! Something went wrong: ${error.message}. Please check your API key.`, "bot");
+ }
+ }
+
+ function addChatMessage(text, sender, isThinking = false) {
+ const msgDiv = document.createElement("div");
+ msgDiv.className = `boxy-chat-message boxy-message-${sender}`;
+ if (isThinking) msgDiv.classList.add("boxy-thinking");
+
+ msgDiv.innerHTML = `
+ ${sender === "bot" ? "🤖" : "👤"}
+ ${text}
+ `;
+
+ messagesContainer.appendChild(msgDiv);
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+ }
+
+ sendBtn.addEventListener("click", sendMessage);
+ input.addEventListener("keypress", (e) => {
+ if (e.key === "Enter") {
+ sendMessage();
+ }
+ });
+ }
+
+ // Expose API for future integrations
+ window.boxyAPI = {
+ showMessage: showBoxyMessage,
+ moveTo: (x, y) => {
+ // Reset offsets when programmatically moving
+ xOffset = x;
+ yOffset = y;
+ boxyCharacter.style.transform = `translate3d(${x}px, ${y}px, 0)`;
+ updateBubblePosition();
+ },
+ openChat: () => {
+ if (chatInterface.style.display === "none") {
+ toggleChat();
+ }
+ },
+ closeChat: () => {
+ chatInterface.style.display = "none";
+ }
+ };
+
+ // Cleanup on addon disable
+ addon.self.addEventListener("disabled", () => {
+ if (boxyHideTimeoutId !== null) {
+ clearTimeout(boxyHideTimeoutId);
+ }
+ if (window.boxyAPI) {
+ delete window.boxyAPI;
+ }
+ });
+
+ console.log("Boxy AI Assistant fully initialized!");
+}
diff --git a/src/addons/generated/addon-entries.js b/src/addons/generated/addon-entries.js
index 2a369010f..9d0bfad51 100644
--- a/src/addons/generated/addon-entries.js
+++ b/src/addons/generated/addon-entries.js
@@ -81,4 +81,5 @@ export default {
"tw-disable-cloud-variables": () => import(/* webpackChunkName: "addon-entry-tw-disable-cloud-variables" */ "../addons/tw-disable-cloud-variables/_runtime_entry.js"),
"tw-disable-compiler": () => import(/* webpackChunkName: "addon-entry-tw-disable-compiler" */ "../addons/tw-disable-compiler/_runtime_entry.js"),
"editor-stepping": () => import(/* webpackChunkName: "addon-entry-editor-stepping" */ "../addons/editor-stepping/_runtime_entry.js"),
+ "boxy-assistant": () => import(/* webpackChunkName: "addon-entry-boxy-assistant" */ "../addons/boxy-assistant/_runtime_entry.js"),
};
diff --git a/src/addons/generated/addon-manifests.js b/src/addons/generated/addon-manifests.js
index 9f0a4ea1a..5a578b40c 100644
--- a/src/addons/generated/addon-manifests.js
+++ b/src/addons/generated/addon-manifests.js
@@ -79,6 +79,7 @@ import _tw_remove_feedback from "../addons/tw-remove-feedback/_manifest_entry.js
import _tw_disable_cloud_variables from "../addons/tw-disable-cloud-variables/_manifest_entry.js";
import _tw_disable_compiler from "../addons/tw-disable-compiler/_manifest_entry.js";
import _editor_stepping from "../addons/editor-stepping/_manifest_entry.js";
+import _boxy_assistant from "../addons/boxy-assistant/_manifest_entry.js";
import santa from "../addons/santa/_manifest_entry.js";
export default {
santa,
@@ -162,4 +163,5 @@ export default {
"tw-disable-cloud-variables": _tw_disable_cloud_variables,
"tw-disable-compiler": _tw_disable_compiler,
"editor-stepping": _editor_stepping,
+ "boxy-assistant": _boxy_assistant,
};
diff --git a/test/unit/addons/boxy-assistant.test.js b/test/unit/addons/boxy-assistant.test.js
new file mode 100644
index 000000000..ad5e6fd1d
--- /dev/null
+++ b/test/unit/addons/boxy-assistant.test.js
@@ -0,0 +1,53 @@
+/**
+ * @jest-environment jsdom
+ */
+
+describe('Boxy Assistant Addon', () => {
+ test('addon manifest exists and has correct structure', () => {
+ const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default;
+
+ expect(manifest).toBeDefined();
+ expect(manifest.name).toBe('Boxy AI Assistant');
+ expect(manifest.editorOnly).toBe(true);
+ expect(manifest.noTranslations).toBe(true);
+ expect(manifest.enabledByDefault).toBe(false);
+ expect(manifest.tags).toContain('featured');
+ expect(manifest.tags).toContain('new');
+ });
+
+ test('addon has required files defined', () => {
+ const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default;
+
+ expect(manifest.userscripts).toBeDefined();
+ expect(manifest.userscripts.length).toBe(1);
+ expect(manifest.userscripts[0].url).toBe('userscript.js');
+
+ expect(manifest.userstyles).toBeDefined();
+ expect(manifest.userstyles.length).toBe(1);
+ expect(manifest.userstyles[0].url).toBe('style.css');
+ });
+
+ test('addon is registered in addons.js', () => {
+ const addonsModule = require('../../../src/addons/addons.js');
+
+ expect(addonsModule.addons).toContain('boxy-assistant');
+ expect(addonsModule.newAddons).toContain('boxy-assistant');
+ });
+
+ test('addon has proper credits', () => {
+ const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default;
+
+ expect(manifest.credits).toBeDefined();
+ expect(manifest.credits.length).toBeGreaterThan(0);
+ expect(manifest.credits[0].name).toBe('supervoidcoder');
+ });
+
+ test('addon description is informative', () => {
+ const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default;
+
+ expect(manifest.description).toBeDefined();
+ expect(manifest.description.length).toBeGreaterThan(50);
+ expect(manifest.description).toContain('AI');
+ expect(manifest.description).toContain('Boxy');
+ });
+});