A character counter for message length when messaging Kins via browser
// ==UserScript==
// @name Kindroid Character Counter
// @namespace http://tampermonkey.net/
// @version 1.1
// @description A character counter for message length when messaging Kins via browser
// @match https://kindroid.ai/chat*
// @grant none
// @author Valco (Discord @alphawuff)
// @license GNU GPLv3
// ==/UserScript==
(function () {
'use strict';
const MAX_CHARS = 4000;
const ACCENT = "#8271f2";
function findElements() {
const input = document.querySelector("textarea");
const button = document.querySelector(".chakra-button.css-s31pb8");
return { input, button };
}
function createCounter(input, button) {
if (document.querySelector(".kindroid-counter")) return;
const counterWrapper = document.createElement('div');
counterWrapper.className = "kindroid-counter";
counterWrapper.style.position = 'fixed';
counterWrapper.style.zIndex = '9999';
counterWrapper.style.pointerEvents = 'none';
const size = 42;
const strokeWidth = 4;
const radius = (size / 2) - strokeWidth - 1;
const circumference = 2 * Math.PI * radius;
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", size);
svg.setAttribute("height", size);
const bgCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
bgCircle.setAttribute("cx", size / 2);
bgCircle.setAttribute("cy", size / 2);
bgCircle.setAttribute("r", radius);
bgCircle.setAttribute("stroke", "rgba(255,255,255,0.15)");
bgCircle.setAttribute("stroke-width", strokeWidth);
bgCircle.setAttribute("fill", "none");
const progressCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
progressCircle.setAttribute("cx", size / 2);
progressCircle.setAttribute("cy", size / 2);
progressCircle.setAttribute("r", radius);
progressCircle.setAttribute("stroke", ACCENT);
progressCircle.setAttribute("stroke-width", strokeWidth);
progressCircle.setAttribute("fill", "none");
progressCircle.setAttribute("stroke-dasharray", circumference);
progressCircle.setAttribute("stroke-dashoffset", circumference);
progressCircle.style.transition = "all 0.2s ease";
progressCircle.style.filter = `drop-shadow(0 0 4px ${ACCENT})`;
progressCircle.setAttribute("transform", `rotate(-90 ${size / 2} ${size / 2})`);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", "50%");
text.setAttribute("y", "54%");
text.setAttribute("dominant-baseline", "middle");
text.setAttribute("text-anchor", "middle");
text.setAttribute("fill", "white");
text.style.fontFamily = "system-ui, sans-serif";
text.style.fontWeight = "700";
text.style.fontSize = "14px";
text.style.paintOrder = "stroke";
text.style.textShadow = "0 0 4px rgba(0,0,0,0.8), 0 0 2px rgba(0,0,0,0.6)";
text.textContent = "0";
svg.appendChild(bgCircle);
svg.appendChild(progressCircle);
svg.appendChild(text);
counterWrapper.appendChild(svg);
document.body.appendChild(counterWrapper);
function positionCounter() {
if (!button) return;
const rect = button.getBoundingClientRect();
counterWrapper.style.left = `${rect.right + 10}px`;
counterWrapper.style.top = `${rect.top + rect.height / 2 - size / 2}px`;
}
function update() {
const length = input.value.length;
const progress = Math.min(length / MAX_CHARS, 1);
progressCircle.setAttribute(
"stroke-dashoffset",
circumference * (1 - progress)
);
text.textContent = length;
const digits = length.toString().length;
let fontSize = 14;
if (digits >= 4) fontSize = 10;
else if (digits === 3) fontSize = 12;
text.style.fontSize = fontSize + "px";
if (length >= MAX_CHARS) {
progressCircle.setAttribute("stroke", "#ff4d4f");
} else if (length > 3000) {
progressCircle.setAttribute("stroke", "#facc15");
} else {
progressCircle.setAttribute("stroke", ACCENT);
}
}
input.addEventListener("input", update);
window.addEventListener("resize", positionCounter);
window.addEventListener("scroll", positionCounter);
const observer = new MutationObserver(() => {
const { button: newButton } = findElements();
if (newButton) button = newButton;
positionCounter();
});
observer.observe(document.body, { childList: true, subtree: true });
positionCounter();
update();
}
function init() {
const observer = new MutationObserver(() => {
const { input, button } = findElements();
if (input && button) {
createCounter(input, button);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
init();
})();