modularise chat commands and add error handling for wrong commands

This commit is contained in:
Fernando Campione
2026-01-09 10:28:50 +00:00
parent 0f08522e56
commit ddcf67226f
8 changed files with 359 additions and 292 deletions
+25
View File
@@ -0,0 +1,25 @@
# Chat Commands
Chat commands are modular and located in `public/js/chat-commands/`.
## Adding a Command
1. Create `chat-commands/mycommand.js`:
```javascript
export const myCommand = {
description: "What it does",
execute: () => {
const output = document.getElementById("terminal-output");
// implementation
},
};
```
2. Import and register in `commands.js`:
```javascript
import { myCommand } from "./chat-commands/mycommand.js";
const actions = {
// ... existing commands
"/mycommand": myCommand,
};
+20 -6
View File
@@ -466,7 +466,7 @@ const appendMessage = (msg) => {
senderName = msg.sender.slice(-8);
}
// Update name map
// Update name map (before changing senderName to "You")
nameToId.set(senderName, msg.sender);
if (msg.sender === myId) senderName = "You";
@@ -491,7 +491,7 @@ const appendMessage = (msg) => {
const contentSpan = document.createElement("span");
contentSpan.className = "msg-content";
// Use formatMessage for rich text rendering
const rawContent = ` > ${msg.content}`;
if (window.ChatCommands) {
contentSpan.innerHTML = window.ChatCommands.formatMessage(rawContent);
@@ -523,13 +523,24 @@ terminalInput.addEventListener("keypress", async (e) => {
if (window.ChatCommands) {
const result = window.ChatCommands.processInput(content);
if (result.type === "action") {
window.ChatCommands.actions[result.command].execute();
const action = window.ChatCommands.actions[result.command];
if (action && typeof action.execute === "function") {
try {
action.execute();
} catch (err) {
console.error("Command execution error:", err);
systemStatusBar.innerText = `[SYSTEM] Command failed: ${result.command}`;
}
} else {
systemStatusBar.innerText = `[SYSTEM] Unknown command: ${result.command}`;
}
return;
} else if (result.type === "text") {
content = result.content;
}
}
let scope = "GLOBAL";
let target = null;
@@ -594,10 +605,11 @@ terminalInput.addEventListener("keypress", async (e) => {
target = nameToId.get(potentialName);
content = msg;
scope = "GLOBAL";
} else if (!msg) {
systemStatusBar.innerText = `[SYSTEM] Usage: /${potentialName} <message>`;
return;
} else {
// If it looks like a command but we don't recognize the user or command
// Prevent sending it as raw text to chat
systemStatusBar.innerText = `[SYSTEM] Unknown command or user: ${potentialName}`;
systemStatusBar.innerText = `[SYSTEM] Unknown user: ${potentialName}`;
return;
}
}
@@ -805,4 +817,6 @@ function cycleTheme() {
}
}
window.cycleTheme = cycleTheme;
document.getElementById("theme-switcher").addEventListener("click", cycleTheme);
+127 -152
View File
@@ -1,90 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<title>Hypermind</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=palette"
/>
<script>
(function () {
var savedTheme =
localStorage.getItem("hypermind-theme") || "default.css";
document.write(
'<link rel="stylesheet" href="/themes/' +
savedTheme +
'" id="theme-css">'
);
})();
</script>
<link rel="stylesheet" href="/style.css" />
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="/js/lists.js"></script>
<script src="/js/screenname.js"></script>
<script src="/js/commands.js"></script>
<script src="/js/version-checker.js"></script>
</head>
<body data-version="{{VERSION}}">
<div id="version-notification" class="version-notification">
<div class="version-content">
<span class="version-text">New version available!</span>
<a href="#" target="_blank" class="update-link">View Release</a>
<button class="dismiss-btn" title="Dismiss">×</button>
</div>
</div>
<canvas id="network" data-visual-limit="{{VISUAL_LIMIT}}"></canvas>
<div class="container">
<div id="count" class="count" data-initial-count="{{COUNT}}">
{{COUNT}}
</div>
<div class="label">Active Nodes</div>
<div class="footer">
powered by
<a
href="https://github.com/lklynet/hypermind"
target="_blank"
class="footer-link"
>hypermind</a
>
</div>
<div class="debug">
ID: <span id="my-screenname">{{ID}}</span><br />
Direct Connections: <span id="direct">{{DIRECT}}</span><br />
Total Unique: <span id="total-unique">{{TOTAL_UNIQUE}}</span><br />
<span class="debug-link" onclick="openDiagnostics()">diagnostics</span>
<span id="map-container" class="{{MAP_CLASS}}">
|
<span class="debug-link" id="map-link" onclick="openMap()"
>map</span
></span
>
</div>
</div>
<head>
<title>Hypermind</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=palette" />
<script>
(function () {
var savedTheme =
localStorage.getItem("hypermind-theme") || "default.css";
document.write(
'<link rel="stylesheet" href="/themes/' +
savedTheme +
'" id="theme-css">'
);
})();
</script>
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="/js/lists.js"></script>
<script src="/js/screenname.js"></script>
<script type="module" src="/js/commands.js"></script>
<script src="/js/version-checker.js"></script>
</head>
<div id="mapModal" class="modal">
<div class="modal-content map-content">
<button class="close-btn" onclick="closeMap()">×</button>
<div id="map"></div>
</div>
<body data-version="{{VERSION}}">
<div id="version-notification" class="version-notification">
<div class="version-content">
<span class="version-text">New version available!</span>
<a href="#" target="_blank" class="update-link">View Release</a>
<button class="dismiss-btn" title="Dismiss">×</button>
</div>
</div>
<div id="diagnosticsModal" class="modal">
<div class="modal-content">
<button class="close-btn" onclick="closeDiagnostics()">×</button>
<div class="modal-title">Diagnostics</div>
<div class="stat-row">
<span class="stat-label">ID</span>
<span
class="stat-value"
id="diag-id"
style="
<canvas id="network" data-visual-limit="{{VISUAL_LIMIT}}"></canvas>
<div class="container">
<div id="count" class="count" data-initial-count="{{COUNT}}">
{{COUNT}}
</div>
<div class="label">Active Nodes</div>
<div class="footer">
powered by
<a href="https://github.com/lklynet/hypermind" target="_blank" class="footer-link">hypermind</a>
</div>
<div class="debug">
ID: <span id="my-screenname">{{ID}}</span><br />
Direct Connections: <span id="direct">{{DIRECT}}</span><br />
Total Unique: <span id="total-unique">{{TOTAL_UNIQUE}}</span><br />
<span class="debug-link" onclick="openDiagnostics()">diagnostics</span>
<span id="map-container" class="{{MAP_CLASS}}">
|
<span class="debug-link" id="map-link" onclick="openMap()">map</span></span>
</div>
</div>
<div id="mapModal" class="modal">
<div class="modal-content map-content">
<button class="close-btn" onclick="closeMap()">×</button>
<div id="map"></div>
</div>
</div>
<div id="diagnosticsModal" class="modal">
<div class="modal-content">
<button class="close-btn" onclick="closeDiagnostics()">×</button>
<div class="modal-title">Diagnostics</div>
<div class="stat-row">
<span class="stat-label">ID</span>
<span class="stat-value" id="diag-id" style="
font-size: 0.8em;
white-space: nowrap;
overflow: hidden;
@@ -92,81 +78,70 @@
direction: rtl;
max-width: 300px;
display: block;
"
>{{FULL_ID}}</span
>
</div>
<div class="stat-row">
<span class="stat-label">Heartbeats Received</span>
<span class="stat-value" id="diag-heartbeats-rx">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Heartbeats Relayed</span>
<span class="stat-value" id="diag-heartbeats-tx">0</span>
</div>
<div class="stat-row">
<span class="stat-label">New Peers Added</span>
<span class="stat-value" id="diag-new-peers">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Duplicate/Old Seq</span>
<span class="stat-value" id="diag-dup-seq">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Invalid PoW</span>
<span class="stat-value" id="diag-invalid-pow">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Invalid Signatures</span>
<span class="stat-value" id="diag-invalid-sig">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Bandwidth In</span>
<span class="stat-value" id="diag-bandwidth-in">0 KB</span>
</div>
<div class="stat-row">
<span class="stat-label">Bandwidth Out</span>
<span class="stat-value" id="diag-bandwidth-out">0 KB</span>
</div>
<div class="stat-row">
<span class="stat-label">LEAVE Messages</span>
<span class="stat-value" id="diag-leave">0</span>
</div>
<div class="update-time" id="last-update">last 10 seconds</div>
">{{FULL_ID}}</span>
</div>
<div class="stat-row">
<span class="stat-label">Heartbeats Received</span>
<span class="stat-value" id="diag-heartbeats-rx">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Heartbeats Relayed</span>
<span class="stat-value" id="diag-heartbeats-tx">0</span>
</div>
<div class="stat-row">
<span class="stat-label">New Peers Added</span>
<span class="stat-value" id="diag-new-peers">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Duplicate/Old Seq</span>
<span class="stat-value" id="diag-dup-seq">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Invalid PoW</span>
<span class="stat-value" id="diag-invalid-pow">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Invalid Signatures</span>
<span class="stat-value" id="diag-invalid-sig">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Bandwidth In</span>
<span class="stat-value" id="diag-bandwidth-in">0 KB</span>
</div>
<div class="stat-row">
<span class="stat-label">Bandwidth Out</span>
<span class="stat-value" id="diag-bandwidth-out">0 KB</span>
</div>
<div class="stat-row">
<span class="stat-label">LEAVE Messages</span>
<span class="stat-value" id="diag-leave">0</span>
</div>
<div class="update-time" id="last-update">last 10 seconds</div>
</div>
</div>
<button
id="theme-switcher"
class="theme-btn {{THEMES_CLASS}}"
title="Cycle Themes"
>
<span class="material-symbols-outlined">palette</span>
<button id="theme-switcher" class="theme-btn {{THEMES_CLASS}}" title="Cycle Themes">
<span class="material-symbols-outlined">palette</span>
</button>
<div id="terminal" class="terminal hidden">
<div id="terminal-resizer" class="terminal-resizer"></div>
<button id="terminal-toggle" class="terminal-toggle" title="Toggle Chat">
</button>
<div id="terminal" class="terminal hidden">
<div id="terminal-resizer" class="terminal-resizer"></div>
<button id="terminal-toggle" class="terminal-toggle" title="Toggle Chat">
</button>
<div id="system-status-bar" class="system-status-bar"></div>
<div id="terminal-output" class="terminal-output"></div>
<div class="terminal-input-line">
<span class="prompt">&gt;</span>
<input
type="text"
id="terminal-input"
maxlength="140"
placeholder="Broadcast..."
autocomplete="off"
/>
</div>
<div id="system-status-bar" class="system-status-bar"></div>
<div id="terminal-output" class="terminal-output"></div>
<div class="terminal-input-line">
<span class="prompt">&gt;</span>
<input type="text" id="terminal-input" maxlength="140" placeholder="Broadcast..." autocomplete="off" />
</div>
</div>
<div id="contextMenu" class="context-menu">
<div class="context-menu-item" id="contextWhisper">Whisper</div>
<div class="context-menu-item" id="contextBlock">Block</div>
</div>
<div id="contextMenu" class="context-menu">
<div class="context-menu-item" id="contextWhisper">Whisper</div>
<div class="context-menu-item" id="contextBlock">Block</div>
</div>
<script src="/app.js"></script>
</body>
</html>
<script src="/app.js"></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
export const clearCommand = {
description: "Clears the chat history",
execute: () => {
const output = document.getElementById("terminal-output");
if (output) output.innerHTML = "";
},
};
+43
View File
@@ -0,0 +1,43 @@
export const frenzyCommand = {
description: "Rotates all themes for 20 seconds",
execute: () => {
const output = document.getElementById("terminal-output");
if (!output) return;
if (typeof window.cycleTheme !== "function") {
const errorDiv = document.createElement("div");
errorDiv.className = "system-message";
errorDiv.style.color = "#ff6b6b";
errorDiv.innerText = "[SYSTEM] Theme frenzy unavailable";
output.appendChild(errorDiv);
output.scrollTop = output.scrollHeight;
return;
}
const startDiv = document.createElement("div");
startDiv.className = "system-message";
startDiv.style.color = "#4ade80";
startDiv.innerText = "[SYSTEM] Hold onto your trousers!!!";
output.appendChild(startDiv);
output.scrollTop = output.scrollHeight;
let count = 0;
const maxRotations = 100;
const frenzyInterval = setInterval(() => {
window.cycleTheme();
count++;
if (count >= maxRotations) {
clearInterval(frenzyInterval);
const endDiv = document.createElement("div");
endDiv.className = "system-message";
endDiv.style.color = "#4ade80";
endDiv.innerText = "[SYSTEM] You survived the storm!";
output.appendChild(endDiv);
output.scrollTop = output.scrollHeight;
}
}, 100);
},
};
+76
View File
@@ -0,0 +1,76 @@
export const helpCommand = {
description: "Shows available commands",
execute: () => {
const output = document.getElementById("terminal-output");
if (!output) return;
const createHelpSection = (title, items) => {
const section = document.createElement("div");
section.style.marginBottom = "10px";
section.style.color = "#aaa";
const header = document.createElement("div");
header.style.fontWeight = "bold";
header.style.color = "#fff";
header.style.marginBottom = "4px";
header.innerText = title;
section.appendChild(header);
items.forEach((item) => {
const div = document.createElement("div");
div.innerHTML = `<span style="color: #4ade80">${item.cmd}</span> - ${item.desc}`;
section.appendChild(div);
});
return section;
};
const helpContainer = document.createElement("div");
helpContainer.className = "system-message";
helpContainer.style.padding = "10px";
helpContainer.style.borderTop = "1px dashed #333";
helpContainer.style.borderBottom = "1px dashed #333";
helpContainer.style.margin = "10px 0";
// system
const systemCmds = [
{
cmd: "/whisper &lt;user&gt; &lt;msg&gt;",
desc: "Send a private message",
},
{ cmd: "/block &lt;user&gt;", desc: "Block messages from a user" },
{ cmd: "/unblock &lt;user&gt;", desc: "Unblock a user" },
{
cmd: "/local &lt;msg&gt;",
desc: "Send message to direct peers only (Global by default)",
},
{ cmd: "/clear", desc: "Clear chat history" },
{ cmd: "/help", desc: "Show this help menu" },
];
helpContainer.appendChild(
createHelpSection("System Commands", systemCmds)
);
// Formatting
const formatCmds = [
{ cmd: "**text**", desc: "Bold" },
{ cmd: "*text*", desc: "Italics" },
{ cmd: "__text__", desc: "Underline" },
{ cmd: "~~text~~", desc: "Strikethrough" },
{ cmd: "`text`", desc: "Code" },
];
helpContainer.appendChild(createHelpSection("Formatting", formatCmds));
// Easter Eggs
const eggs = Object.entries(window.ChatCommands.replacements).map(
([k, v]) => ({
cmd: k,
desc: v,
})
);
helpContainer.appendChild(createHelpSection("Easter Eggs", eggs));
output.appendChild(helpContainer);
output.scrollTop = output.scrollHeight;
},
};
+10
View File
@@ -0,0 +1,10 @@
export const replacements = {
"/shrug": "¯\\_(ツ)_/¯",
"/heart": "♡",
"/tableflip": "(╯°□°)╯︵ ┻━┻",
"/unflip": "┬─┬ ( ゜-゜ノ)",
"/lenny": "( ͡° ͜ʖ ͡°)",
"/gimme": "༼ つ ◕_◕ ༽つ",
"/disapproval": "ಠ_ಠ",
"/magic": "(ノ◕ヮ◕)ノ*:・゚✧",
};
+51 -134
View File
@@ -1,139 +1,56 @@
// Chat Commands and Easter Eggs Configuration
/**
* If you are a developer, import all commands individually here.
* This way the chat can scale and improve over time without making it a mess
*/
import { clearCommand } from "./chat-commands/clear.js";
import { frenzyCommand } from "./chat-commands/frenzy.js";
import { helpCommand } from "./chat-commands/help.js";
import { replacements } from "./chat-commands/replacements.js";
const formatMessage = (text) => {
if (!text) return "";
let html = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
html = html.replace(/__(.*?)__/g, "<u>$1</u>");
html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
html = html.replace(/`(.*?)`/g, "<code>$1</code>");
return html;
};
const actions = {
"/clear": clearCommand,
"/frenzy": frenzyCommand,
"/help": helpCommand,
};
const processInput = (input) => {
if (actions[input]) {
return { type: "action", command: input };
}
let processed = input;
for (const [cmd, replacement] of Object.entries(replacements)) {
const regex = new RegExp(cmd, "g");
processed = processed.replace(regex, replacement);
}
return { type: "text", content: processed };
};
const ChatCommands = {
// local
actions: {
"/clear": {
description: "Clears the chat history",
execute: () => {
const output = document.getElementById("terminal-output");
if (output) output.innerHTML = "";
},
},
"/help": {
description: "Shows available commands",
execute: () => {
const output = document.getElementById("terminal-output");
if (!output) return;
const createHelpSection = (title, items) => {
const section = document.createElement("div");
section.style.marginBottom = "10px";
section.style.color = "#aaa";
const header = document.createElement("div");
header.style.fontWeight = "bold";
header.style.color = "#fff";
header.style.marginBottom = "4px";
header.innerText = title;
section.appendChild(header);
items.forEach((item) => {
const div = document.createElement("div");
div.innerHTML = `<span style="color: #4ade80">${item.cmd}</span> - ${item.desc}`;
section.appendChild(div);
});
return section;
};
const helpContainer = document.createElement("div");
helpContainer.className = "system-message";
helpContainer.style.padding = "10px";
helpContainer.style.borderTop = "1px dashed #333";
helpContainer.style.borderBottom = "1px dashed #333";
helpContainer.style.margin = "10px 0";
// system
const systemCmds = [
{
cmd: "/whisper &lt;user&gt; &lt;msg&gt;",
desc: "Send a private message",
},
{ cmd: "/block &lt;user&gt;", desc: "Block messages from a user" },
{ cmd: "/unblock &lt;user&gt;", desc: "Unblock a user" },
{
cmd: "/local &lt;msg&gt;",
desc: "Send message to direct peers only (Global by default)",
},
{ cmd: "/clear", desc: "Clear chat history" },
{ cmd: "/help", desc: "Show this help menu" },
];
helpContainer.appendChild(
createHelpSection("System Commands", systemCmds)
);
// Formatting
const formatCmds = [
{ cmd: "**text**", desc: "Bold" },
{ cmd: "*text*", desc: "Italics" },
{ cmd: "__text__", desc: "Underline" },
{ cmd: "~~text~~", desc: "Strikethrough" },
{ cmd: "`text`", desc: "Code" },
];
helpContainer.appendChild(createHelpSection("Formatting", formatCmds));
// Easter Eggs
const eggs = Object.entries(ChatCommands.replacements).map(
([k, v]) => ({
cmd: k,
desc: v,
})
);
helpContainer.appendChild(createHelpSection("Easter Eggs", eggs));
output.appendChild(helpContainer);
output.scrollTop = output.scrollHeight;
},
},
},
// eastereggs
replacements: {
"/shrug": "¯\\_(ツ)_/¯",
"/heart": "♡",
"/tableflip": "(╯°□°)╯︵ ┻━┻",
"/unflip": "┬─┬ ( ゜-゜ノ)",
"/lenny": "( ͡° ͜ʖ ͡°)",
"/gimme": "༼ つ ◕_◕ ༽つ",
"/disapproval": "ಠ_ಠ",
"/magic": "(ノ◕ヮ◕)ノ*:・゚✧",
},
processInput: (input) => {
if (ChatCommands.actions[input]) {
return { type: "action", command: input };
}
let processed = input;
for (const [cmd, replacement] of Object.entries(
ChatCommands.replacements
)) {
const regex = new RegExp(cmd, "g");
processed = processed.replace(regex, replacement);
}
return { type: "text", content: processed };
},
formatMessage: (text) => {
if (!text) return "";
let html = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
html = html.replace(/__(.*?)__/g, "<u>$1</u>");
html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
html = html.replace(/`(.*?)`/g, "<code>$1</code>");
return html;
},
actions,
replacements,
processInput,
formatMessage,
};
window.ChatCommands = ChatCommands;