diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index 2362eca..c1abd58 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -1,5 +1,5 @@ import { app } from "./app.js"; -import { NjetComponent,eventBus } from "./njet.js"; +import { NjetComponent, eventBus } from "./njet.js"; import { FileUploadGrid } from "./file-upload-grid.js"; class ChatInputComponent extends NjetComponent { @@ -10,15 +10,15 @@ class ChatInputComponent extends NjetComponent { hiddenCompletions = { "/starsRender": () => { - app.rpc.starsRender(this.channelUid, this.value.replace("/starsRender ", "")) + app.rpc.starsRender(this.channelUid, this.value.replace("/starsRender ", "")); }, "/leet": () => { this.value = this.textToLeet(this.value); - this._leetSpeak = !this._leetSpeak; + this._leetSpeak = !this._leetSpeak; }, "/l33t": () => { this._leetSpeakAdvanced = !this._leetSpeakAdvanced; - } + }, }; users = []; @@ -29,11 +29,12 @@ class ChatInputComponent extends NjetComponent { lastMessagePromise = null; _leetSpeak = false; _leetSpeakAdvanced = false; + constructor() { super(); this.lastUpdateEvent = new Date(); this.textarea = document.createElement("textarea"); - this.textarea.classList.add("chat-input-textarea"); + this.textarea.classList.add("chat-input-textarea"); this.value = this.getAttribute("value") || ""; } @@ -47,7 +48,7 @@ class ChatInputComponent extends NjetComponent { } get allAutoCompletions() { - return Object.assign({}, this.autoCompletions, this.hiddenCompletions); + return { ...this.autoCompletions, ...this.hiddenCompletions }; } resolveAutoComplete(input) { @@ -78,16 +79,90 @@ class ChatInputComponent extends NjetComponent { } extractMentions(text) { - return Array.from(text.matchAll(/@([a-zA-Z0-9_-]+)/g), m => m[1]); + return Array.from(text.matchAll(/@([a-zA-Z0-9_-]+)/g), (m) => m[1]); } matchMentionsToAuthors(mentions, authors) { - return mentions.map(mention => { + return mentions.map((mention) => { + const lowerMention = mention.toLowerCase(); + let bestMatch = null; + let bestScore = 0; + + for (const author of authors) { + const lowerAuthor = author.toLowerCase(); + let score = 0; + + if (lowerMention === lowerAuthor) { + score = 100; + } else if (lowerAuthor.startsWith(lowerMention)) { + score = 90 + (5 * (lowerMention.length / lowerAuthor.length)); + } else if (lowerAuthor.includes(lowerMention) && lowerMention.length >= 2) { + const position = lowerAuthor.indexOf(lowerMention); + score = 80 - (10 * (position / lowerAuthor.length)); + } else if (this.isFuzzyMatch(lowerMention, lowerAuthor)) { + const ratio = lowerMention.length / lowerAuthor.length; + score = 40 + (20 * ratio); + } else if (this.isCloseMatch(lowerMention, lowerAuthor)) { + score = 30 + (10 * (lowerMention.length / lowerAuthor.length)); + } + + if (score > bestScore) { + bestScore = score; + bestMatch = author; + } + } + + const minScore = 40; + return { + mention, + closestAuthor: bestScore >= minScore ? bestMatch : null, + score: bestScore, + }; + }); + } + + isFuzzyMatch(needle, haystack) { + if (needle.length < 2) return false; + + let needleIndex = 0; + for (let i = 0; i < haystack.length && needleIndex < needle.length; i++) { + if (haystack[i] === needle[needleIndex]) { + needleIndex++; + } + } + return needleIndex === needle.length; + } + + isCloseMatch(str1, str2) { + if (Math.abs(str1.length - str2.length) > 2) return false; + + const shorter = str1.length <= str2.length ? str1 : str2; + const longer = str1.length > str2.length ? str1 : str2; + + let differences = 0; + let j = 0; + + for (let i = 0; i < shorter.length && j < longer.length; i++) { + if (shorter[i] !== longer[j]) { + differences++; + if (j + 1 < longer.length && shorter[i] === longer[j + 1]) { + j++; + } + } + j++; + } + + differences += Math.abs(longer.length - j); + return differences <= 2; + } + + matchMentions2ToAuthors(mentions, authors) { + return mentions.map((mention) => { let closestAuthor = null; let minDistance = Infinity; const lowerMention = mention.toLowerCase(); - authors.forEach(author => { + authors.forEach((author) => { const lowerAuthor = author.toLowerCase(); let distance = this.levenshteinDistance(lowerMention, lowerAuthor); @@ -98,13 +173,12 @@ class ChatInputComponent extends NjetComponent { if (distance < minDistance) { minDistance = distance; closestAuthor = author; - console.info("closestAuthor",closestAuthor) - console.info("minDistance",minDistance) } }); - if (minDistance < 5){ + + if (minDistance < 5) { closestAuthor = 0; - } + } return { mention, closestAuthor, distance: minDistance }; }); } @@ -112,7 +186,6 @@ class ChatInputComponent extends NjetComponent { levenshteinDistance(a, b) { const matrix = []; - // Initialize the first row and column for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } @@ -120,16 +193,15 @@ class ChatInputComponent extends NjetComponent { matrix[0][j] = j; } - // Fill in the matrix for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( - matrix[i - 1][j] + 1, // Deletion - matrix[i][j - 1] + 1, // Insertion - matrix[i - 1][j - 1] + 1 // Substitution + matrix[i - 1][j] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j - 1] + 1 ); } } @@ -151,100 +223,39 @@ class ChatInputComponent extends NjetComponent { return updatedText; } + textToLeet(text) { - // L33t speak character mapping - const leetMap = { - 'a': '4', - 'A': '4', - 'e': '3', - 'E': '3', - 'i': '1', - 'I': '1', - 'o': '0', - 'O': '0', - 's': '5', - 'S': '5', - 't': '7', - 'T': '7', - 'l': '1', - 'L': '1', - 'g': '9', - 'G': '9', - 'b': '6', - 'B': '6', - 'z': '2', - 'Z': '2' - }; - - // Convert text to l33t speak - return text.split('').map(char => { - return leetMap[char] || char; - }).join(''); -} + const leetMap = { + 'a': '4', 'A': '4', 'e': '3', 'E': '3', 'i': '1', 'I': '1', + 'o': '0', 'O': '0', 's': '5', 'S': '5', 't': '7', 'T': '7', + 'l': '1', 'L': '1', 'g': '9', 'G': '9', 'b': '6', 'B': '6', + 'z': '2', 'Z': '2' + }; + return text.split('').map(char => leetMap[char] || char).join(''); + } -// Advanced version with random character selection -textToLeetAdvanced(text) { - const leetMap = { - 'a': ['4', '@', '/\\'], - 'A': ['4', '@', '/\\'], - 'e': ['3', '€'], - 'E': ['3', '€'], - 'i': ['1', '!', '|'], - 'I': ['1', '!', '|'], - 'o': ['0', '()'], - 'O': ['0', '()'], - 's': ['5', '$'], - 'S': ['5', '$'], - 't': ['7', '+'], - 'T': ['7', '+'], - 'l': ['1', '|'], - 'L': ['1', '|'], - 'g': ['9', '6'], - 'G': ['9', '6'], - 'b': ['6', '|3'], - 'B': ['6', '|3'], - 'z': ['2'], - 'Z': ['2'], - 'h': ['#', '|-|'], - 'H': ['#', '|-|'], - 'n': ['|\\|'], - 'N': ['|\\|'], - 'm': ['|\\/|'], - 'M': ['|\\/|'], - 'w': ['\\/\\/'], - 'W': ['\\/\\/'], - 'v': ['\\/', 'V'], - 'V': ['\\/', 'V'], - 'u': ['|_|'], - 'U': ['|_|'], - 'r': ['|2'], - 'R': ['|2'], - 'f': ['|='], - 'F': ['|='], - 'd': ['|)'], - 'D': ['|)'], - 'c': ['(', '['], - 'C': ['(', '['], - 'k': ['|<'], - 'K': ['|<'], - 'p': ['|>'], - 'P': ['|>'], - 'x': ['><'], - 'X': ['><'], - 'y': ['`/'], - 'Y': ['`/'] - }; - - return text.split('').map(char => { - const options = leetMap[char]; - if (options) { - return options[Math.floor(Math.random() * options.length)]; - } - return char; - }).join(''); -} + textToLeetAdvanced(text) { + const leetMap = { + 'a': ['4', '@', '/\\'], 'A': ['4', '@', '/\\'], 'e': ['3', '€'], + 'E': ['3', '€'], 'i': ['1', '!', '|'], 'I': ['1', '!', '|'], + 'o': ['0', '()'], 'O': ['0', '()'], 's': ['5', '$'], 'S': ['5', '$'], + 't': ['7', '+'], 'T': ['7', '+'], 'l': ['1', '|'], 'L': ['1', '|'], + 'g': ['9', '6'], 'G': ['9', '6'], 'b': ['6', '|3'], 'B': ['6', '|3'], + 'z': ['2'], 'Z': ['2'], 'h': ['#', '|-|'], 'H': ['#', '|-|'], + 'n': ['|\\|'], 'N': ['|\\|'], 'm': ['|\\/|'], 'M': ['|\\/|'], + 'w': ['\\/\\/'], 'W': ['\\/\\/'], 'v': ['\\/', 'V'], 'V': ['\\/', 'V'], + 'u': ['|_|'], 'U': ['|_|'], 'r': ['|2'], 'R': ['|2'], 'f': ['|='], + 'F': ['|='], 'd': ['|)'], 'D': ['|)'], 'c': ['(', '['], 'C': ['(', '['], + 'k': ['|<'], 'K': ['|<'], 'p': ['|>'], 'P': ['|>'], 'x': ['><'], + 'X': ['><'], 'y': ['`/'], 'Y': ['`/'] + }; + return text.split('').map(char => { + const options = leetMap[char]; + return options ? options[Math.floor(Math.random() * options.length)] : char; + }).join(''); + } async connectedCallback() { this.user = null; @@ -252,7 +263,7 @@ textToLeetAdvanced(text) { this.user = user; }); - this.liveType = this.getAttribute("live-type") == "true"; + this.liveType = this.getAttribute("live-type") === "true"; this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 6; this.channelUid = this.getAttribute("channel"); @@ -270,15 +281,14 @@ textToLeetAdvanced(text) { this.textarea.setAttribute("placeholder", "Type a message..."); this.textarea.setAttribute("rows", "2"); - this.appendChild(this.textarea); + this.ttsButton = document.createElement("stt-button"); - this.snekSpeaker = document.createElement("snek-speaker"); this.appendChild(this.snekSpeaker); - this.ttsButton.addEventListener("click", (e) => { - this.snekSpeaker.enable() + this.ttsButton.addEventListener("click", () => { + this.snekSpeaker.enable(); }); this.appendChild(this.ttsButton); @@ -294,34 +304,34 @@ textToLeetAdvanced(text) { e.preventDefault(); this.fileUploadGrid.openFileDialog(); }); - this.subscribe("file-uploading", (e) => { + + this.subscribe("file-uploading", () => { this.fileUploadGrid.style.display = "block"; this.uploadButton.style.display = "none"; this.textarea.style.display = "none"; - }) + }); + this.appendChild(this.uploadButton); this.textarea.addEventListener("blur", () => { - this.updateFromInput(this.value, true).then( - this.updateFromInput("") - ) + this.updateFromInput(this.value, true).then(() => this.updateFromInput("")); }); - this.subscribe("file-uploads-done", (data)=>{ + this.subscribe("file-uploads-done", (data) => { this.textarea.style.display = "block"; this.uploadButton.style.display = "block"; this.fileUploadGrid.style.display = "none"; - let msg =data.reduce((message, file) => { + const msg = data.reduce((message, file) => { return `${message}[${file.filename || file.name || file.remoteFile}](/channel/attachment/${file.remoteFile})`; }, ''); app.rpc.sendMessage(this.channelUid, msg, true); }); - - this.textarea.addEventListener("change",(e)=>{ - this.value = this.textarea.value; + this.textarea.addEventListener("change", (e) => { + this.value = this.textarea.value; this.updateFromInput(e.target.value); - }) + }); + this.textarea.addEventListener("keyup", (e) => { if (e.key === "Enter" && !e.shiftKey) { const message = this.replaceMentionsWithAuthors(this.value); @@ -330,7 +340,7 @@ textToLeetAdvanced(text) { if (!message) { return; } - let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ", 1)[0]]; + const autoCompletionHandler = this.allAutoCompletions[this.value.split(" ", 1)[0]]; if (autoCompletionHandler) { autoCompletionHandler(); this.value = ""; @@ -348,10 +358,9 @@ textToLeetAdvanced(text) { this.textarea.addEventListener("keydown", (e) => { this.value = e.target.value; - let autoCompletion = null; if (e.key === "Tab") { e.preventDefault(); - autoCompletion = this.resolveAutoComplete(this.value); + const autoCompletion = this.resolveAutoComplete(this.value); if (autoCompletion) { e.target.value = autoCompletion; this.value = autoCompletion; @@ -368,11 +377,11 @@ textToLeetAdvanced(text) { } }); - this.addEventListener("upload", (e) => { + this.addEventListener("upload", () => { this.focus(); }); - this.addEventListener("uploaded", function (e) { - let message = e.detail.files.reduce((message, file) => { + this.addEventListener("uploaded", (e) => { + const message = e.detail.files.reduce((message, file) => { return `${message}[${file.name}](/channel/attachment/${file.relative_url})`; }, ''); app.rpc.sendMessage(this.channelUid, message, true); @@ -408,25 +417,21 @@ textToLeetAdvanced(text) { finalizeMessage(messageUid) { let value = this.value; - value = this.replaceMentionsWithAuthors(value) - if(this._leetSpeak){ - value = this.textToLeet(value); - }else if(this._leetSpeakAdvanced){ - value = this.textToLeetAdvanced(value); + value = this.replaceMentionsWithAuthors(value); + if (this._leetSpeak) { + value = this.textToLeet(value); + } else if (this._leetSpeakAdvanced) { + value = this.textToLeetAdvanced(value); } - app.rpc.sendMessage(this.channelUid, value , true); + app.rpc.sendMessage(this.channelUid, value, true); this.value = ""; this.messageUid = null; this.queuedMessage = null; this.lastMessagePromise = null; } - - updateFromInput(value, isFinal = false) { - this.value = value; - this.flagTyping(); if (this.liveType && value[0] !== "/") {