您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Logs the last bot message and shows an emotion-based avatar. Completly customizable.
// ==UserScript== // @name JanitorAI - Dynamic Character Avatars // @namespace http://tampermonkey.net/ // @version 1.7 // @license MIT // @author Zephyr (@xzeph__) // @description Logs the last bot message and shows an emotion-based avatar. Completly customizable. // @match https://janitorai.com/chats/* // @icon https://www.google.com/s2/favicons?sz=64&domain=janitorai.com // @grant GM_log // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect generativelanguage.googleapis.com // ==/UserScript== /* 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 * * * JanitorAI - Dynamic Character Avatars Script * * * * This script adds dynamic, emotion-based avatars to JanitorAI chats! * * It analyzes the bot's last message and shows a cute avatar matching * * the detected emotion. * * * * HOW TO CUSTOMIZE * * ───────────────────── * * • SECTION 1: Change/add/remove your character avatars and emotion images. * * • SECTION 2: Set your Gemini API key and model. * * • SECTION 5: Edit the prompt sent to Gemini for emotion analysis. * * (Make sure the emotion list here matches your avatars in Section 1!) * * * * All other sections are core logic and style. * * For best results, only edit Sections 1, 2, and 5! * * * * * * 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 。・゚゚・。 */ (function () { "use strict"; /* *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* * * * SECTION 1: AVATAR CONFIGURATION * * (Customize your character emotion avatars here!) * * * *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* */ // 🖼️ --- Character Emotion Avatars --- 🖼️ const characterEmotionAvatars = { Ashley: { 1: "https://i.ibb.co/BHNGbwC2/proud.webp", // Admiration 2: "https://i.ibb.co/XZFRjgWX/amusement.webp", // Amusement 3: "https://i.ibb.co/qYQvPc7g/yelling-insulting-angry-alittlelessintense-arguingagressive.webp", // Anger 4: "https://i.ibb.co/mV297s2V/irritated-annoyed.webp", // Annoyance 5: "https://i.ibb.co/tpRmY6Nj/chatty-hasalittlesmilewhiletalking.webp", // Approval 6: "https://i.ibb.co/tMztWz5L/gentle.webp", // Caring 7: "https://i.ibb.co/39YzgMdV/embarrassment.webp", // Confusion 8: "https://files.catbox.moe/m99dz8.png", // Curiosity 9: "https://i.ibb.co/4gMkVBmb/teasing-mockingaffection.webp", // Desire 10: "https://i.ibb.co/mV297s2V/irritated-annoyed.webp", // Dissapointment 11: "https://i.ibb.co/j981cRP2/exasperation.webp", // Disapproval 12: "https://i.ibb.co/HTZgC02b/disgusted.webp", // Disgust 13: "https://i.ibb.co/39YzgMdV/embarrassment.webp", // Embarrassment 14: "https://i.ibb.co/6J8WYw9T/excited.webp", // Excitement 15: "https://i.ibb.co/FL2HgSRY/terrified.webp", // Fear 16: "https://i.ibb.co/tpRmY6Nj/chatty-hasalittlesmilewhiletalking.webp", // Gratitude 17: "https://i.ibb.co/Xx94PTxr/smiling.webp", // Joy 18: "https://i.ibb.co/tMztWz5L/gentle.webp", // Love 19: "https://i.ibb.co/YFtm7tvZ/nervouslaugh.webp", // Nervousness 20: "https://i.ibb.co/DDSmJHRC/bored-dismissive.webp", // Neutral 21: "https://i.ibb.co/6J8WYw9T/excited.webp", // Optimism 22: "https://i.ibb.co/BHNGbwC2/proud.webp", // Pride 23: "https://i.ibb.co/N6VxqxVy/surprised-confused.webp", // Realization 24: "https://i.ibb.co/k27tvrfK/resignation.webp", // Relief 25: "https://i.ibb.co/JR6S8LNC/weak-unwell-depressed.webp", // Remorse 26: "https://i.ibb.co/tPcnCf41/sad.webp", // Sadness 27: "https://i.ibb.co/NghfF9vc/greatlysurprised-revelation.webp", // Surprise }, // Add other characters and emotions here if needed. The default character is Ashley }; /* *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* * * * SECTION 2: GEMINI API CONFIGURATION * * (Set up your Gemini API key and model here!) * * * *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* */ // 🔑 --- API Key & Model --- 🔑 const apiKey = "YOUR_OWN_API_KEY"; // Replace with your actual API key const model = "gemini-2.0-flash"; const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; /* *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* * * * SECTION 3: AVATAR STYLES & FUNCTIONS * * (Handles avatar CSS, creation, and interaction!) * * * * ⚠️ WARNING: Do not modify the rest of the code unless you know what * * you're doing! This section and below contains core logic and utilities. * * To customize how emotions are detected, go to SECTION 5! * * * *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* */ // 🎨 --- Avatar CSS Styles --- 🎨 GM_addStyle(` .emotion-avatar-item { position: relative; display: flex; flex-direction: column; align-items: center; /* Center children horizontally */ margin-bottom: 5px; } img.emotion-avatar-float { transition: opacity 0.3s ease; cursor: move; width: 100px; height: auto; margin-bottom: 8px; display: block; } .emotion-avatar-controls { position: absolute; top: 0; right: -50px; display: flex; flex-direction: column; align-items: center; gap: 5px; opacity: 0; z-index: 10; transition: opacity 0.3s ease; } .emotion-avatar-controls button { border-radius: 50%; background: linear-gradient(to bottom, rgba(176, 196, 222, 0.8), rgba(255, 255, 255, 0.5)); color: rgba(255, 255, 255, 0.9) !important; box-shadow: 0 0 5px rgba(176, 196, 222, 0.8); transition: box-shadow 0.3s ease-in-out; border: none; width: 40px; height: 40px; padding: 0; margin: 2px 0; cursor: pointer; display: flex; align-items: center; justify-content: center; } .emotion-avatar-controls button:hover { box-shadow: 0 0 10px rgba(176, 196, 222, 1); } .emotion-avatar-slider-fade { opacity: 0; transition: opacity 0.3s ease; width: 100%; margin-top: 0; margin-bottom: 4px; left: 0; position: relative; display: flex; justify-content: center; align-items: center; } /* --- GLASSY SLIDER THEME --- */ .size-slider { -webkit-appearance: none; appearance: none; width: 100px; height: 8px; border-radius: 8px; background: linear-gradient(90deg, rgba(176,196,222,0.7) 0%, rgba(255,255,255,0.5) 100%); box-shadow: 0 2px 8px 0 rgba(176,196,222,0.25), 0 1.5px 0px 0px rgba(255,255,255,0.25) inset; outline: none; opacity: 0.95; transition: box-shadow 0.2s; cursor: pointer; border: 1px solid rgba(176,196,222,0.3); backdrop-filter: blur(2px); display: block; margin-top: 3px; margin-left: auto; margin-right: auto; } .size-slider:focus { box-shadow: 0 0 0 2px rgba(176,196,222,0.5); } .size-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, rgba(255,255,255,0.85) 60%, rgba(176,196,222,0.7) 100%); box-shadow: 0 0 8px 2px rgba(176,196,222,0.7), 0 2px 6px 0 rgba(176,196,222,0.3); border: 2px solid rgba(255,255,255,0.7); cursor: pointer; transition: box-shadow 0.2s; filter: drop-shadow(0 0 4px rgba(176,196,222,0.5)); } .size-slider:active::-webkit-slider-thumb { box-shadow: 0 0 12px 4px rgba(176,196,222,0.9); } .size-slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, rgba(255,255,255,0.85) 60%, rgba(176,196,222,0.7) 100%); box-shadow: 0 0 8px 2px rgba(176,196,222,0.7), 0 2px 6px 0 rgba(176,196,222,0.3); border: 2px solid rgba(255,255,255,0.7); cursor: pointer; transition: box-shadow 0.2s; filter: drop-shadow(0 0 4px rgba(176,196,222,0.5)); } .size-slider:active::-moz-range-thumb { box-shadow: 0 0 12px 4px rgba(176,196,222,0.9); } .size-slider::-ms-thumb { width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, rgba(255,255,255,0.85) 60%, rgba(176,196,222,0.7) 100%); box-shadow: 0 0 8px 2px rgba(176,196,222,0.7), 0 2px 6px 0 rgba(176,196,222,0.3); border: 2px solid rgba(255,255,255,0.7); cursor: pointer; transition: box-shadow 0.2s; filter: drop-shadow(0 0 4px rgba(176,196,222,0.5)); } .size-slider:active::-ms-thumb { box-shadow: 0 0 12px 4px rgba(176,196,222,0.9); } .size-slider::-ms-fill-lower { background: transparent; } .size-slider::-ms-fill-upper { background: transparent; } .size-slider:focus { outline: none; } .size-slider::-webkit-slider-runnable-track { height: 8px; border-radius: 8px; background: transparent; } .size-slider::-moz-range-track { height: 8px; border-radius: 8px; background: transparent; } .size-slider::-ms-fill-lower, .size-slider::-ms-fill-upper { background: transparent; } .slider-value-popup { position: absolute; bottom: 15px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 2px 5px; border-radius: 3px; font-size: 12px; white-space: nowrap; display: none; } `); let botAvatars = {}; let botCount = 0; let scriptDisabled = false; // 🧩 --- Avatar & Gemini Functions --- 🧩 function addEmotionAvatar(emotionNumber, characterName) { if (scriptDisabled) return; if (!botAvatars[characterName]) { const avatarItem = document.createElement("div"); avatarItem.classList.add("emotion-avatar-item"); avatarItem.style.position = "fixed"; avatarItem.style.top = `${50 + botCount * 20}px`; avatarItem.style.left = `${20 + botCount * 20}px`; avatarItem.style.zIndex = 1000 + botCount; const avatar = document.createElement("img"); avatar.classList.add("emotion-avatar-float"); avatar.style.opacity = "1"; // Limit avatar size after image loads avatar.onload = () => { getMaxAvatarSize(avatar, ({ width, height }) => { avatar.style.width = Math.min(100, width) + "px"; avatar.style.maxWidth = width + "px"; avatar.style.maxHeight = height + "px"; if (avatar._sizeSlider) { avatar._sizeSlider.max = width; } }); }; // --- Size Slider --- const sliderContainer = document.createElement("div"); sliderContainer.className = "size-slider-container emotion-avatar-slider-fade"; const sizeSlider = document.createElement("input"); sizeSlider.type = "range"; sizeSlider.min = "80"; sizeSlider.max = "800"; sizeSlider.value = "100"; sizeSlider.className = "size-slider"; sizeSlider.style.width = "100px"; avatar._sizeSlider = sizeSlider; // Limit slider max after image loads getMaxAvatarSize(avatar, ({ width }) => { sizeSlider.max = width; if (parseInt(sizeSlider.value) > width) { sizeSlider.value = width; avatar.style.width = width + "px"; sizeSlider.style.width = Math.max(50, width * 0.8) + "px"; } }); const valuePopup = document.createElement("div"); valuePopup.className = "slider-value-popup"; sliderContainer.appendChild(valuePopup); const updatePopup = () => { valuePopup.textContent = `${sizeSlider.value}px`; const percent = (sizeSlider.value - sizeSlider.min) / (sizeSlider.max - sizeSlider.min); const thumbWidth = 12; const popupLeft = percent * (sizeSlider.offsetWidth - thumbWidth) + thumbWidth / 2; valuePopup.style.left = `${popupLeft}px`; }; const handleSliderInteractionStart = (e) => { e.stopPropagation(); valuePopup.style.display = 'block'; updatePopup(); }; const handleSliderInteractionEnd = () => { valuePopup.style.display = 'none'; }; sizeSlider.addEventListener("mousedown", handleSliderInteractionStart); sizeSlider.addEventListener("touchstart", handleSliderInteractionStart); sizeSlider.addEventListener("input", updatePopup); sizeSlider.addEventListener("mouseup", handleSliderInteractionEnd); sizeSlider.addEventListener("touchend", handleSliderInteractionEnd); sizeSlider.addEventListener("change", () => { // Clamp to max allowed size const maxAllowed = parseInt(sizeSlider.max); let newWidth = Math.min(parseInt(sizeSlider.value), maxAllowed); avatar.style.width = newWidth + "px"; sizeSlider.style.width = Math.max(50, newWidth * 0.8) + "px"; keepInBounds(avatarItem); }); sliderContainer.appendChild(sizeSlider); const controls = addControlButtons(avatarItem, avatar, sliderContainer); avatarItem.appendChild(avatar); avatarItem.appendChild(sliderContainer); avatarItem.appendChild(controls); document.body.appendChild(avatarItem); botAvatars[characterName] = { avatar, container: avatarItem }; makeDraggable(avatarItem); botCount++; } const avatarData = botAvatars[characterName]; if (!avatarData) return; const characterAvatars = characterEmotionAvatars[characterName] || characterEmotionAvatars["Ashley"]; const newSrc = characterAvatars[emotionNumber]; if (newSrc && avatarData.avatar.src !== newSrc) { avatarData.avatar.style.opacity = "0"; setTimeout(() => { avatarData.avatar.src = newSrc; avatarData.avatar.onload = () => { avatarData.avatar.style.opacity = "1"; // Re-limit size on new image getMaxAvatarSize(avatarData.avatar, ({ width, height }) => { avatarData.avatar.style.maxWidth = width + "px"; avatarData.avatar.style.maxHeight = height + "px"; if (avatarData.avatar._sizeSlider) { avatarData.avatar._sizeSlider.max = width; // Clamp current size if needed if (parseInt(avatarData.avatar.style.width) > width) { avatarData.avatar.style.width = width + "px"; avatarData.avatar._sizeSlider.value = width; } } }); }; }, 300); } else if (!newSrc) { console.error(`Emotion number ${emotionNumber} not found for character: ${characterName}`); } } // 🛡️ --- Keep Avatar In Bounds --- 🛡️ function keepInBounds(elmnt) { if (scriptDisabled) return; const rect = elmnt.getBoundingClientRect(); const winWidth = window.innerWidth; const winHeight = window.innerHeight; let newLeft = rect.left; let newTop = rect.top; if (rect.left < 0) newLeft = 0; if (rect.top < 0) newTop = 0; if (rect.right > winWidth) newLeft = winWidth - rect.width; if (rect.bottom > winHeight) newTop = winHeight - rect.height; elmnt.style.left = newLeft + 'px'; elmnt.style.top = newTop + 'px'; } /* *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* * * * SECTION 4: DRAGGABLE & CONTROLS * * (Drag, flip, close, and control avatar UI!) * * * *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* */ // ✋ --- Make Avatar Draggable (Mouse & Touch) --- ✋ function makeDraggable(elmnt) { if (scriptDisabled) return; let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; let isDragging = false; const dragHandle = elmnt; // Assign start events dragHandle.onmousedown = dragStart; dragHandle.ontouchstart = dragStart; function dragStart(e) { // Don't start drag if the target is a button or slider if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') { return; } e = e || window.event; isDragging = false; // Get the initial cursor position if (e.type === 'touchstart') { // --- Prevent page scrolling on drag start --- e.preventDefault(); pos3 = e.touches[0].clientX; pos4 = e.touches[0].clientY; // Assign touch-specific listeners document.ontouchmove = elementDrag; document.ontouchend = dragEnd; } else { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; // Assign mouse-specific listeners document.onmousemove = elementDrag; document.onmouseup = dragEnd; } } function elementDrag(e) { isDragging = true; e = e || window.event; e.preventDefault(); let clientX, clientY; // Get the new cursor position if (e.type === 'touchmove') { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } // Calculate the displacement pos1 = pos3 - clientX; pos2 = pos4 - clientY; pos3 = clientX; pos4 = clientY; // Calculate the new element position const newTop = elmnt.offsetTop - pos2; const newLeft = elmnt.offsetLeft - pos1; // Boundary collision detection const maxLeft = window.innerWidth - elmnt.offsetWidth; const maxTop = window.innerHeight - elmnt.offsetHeight; // Set the element's new position, respecting boundaries elmnt.style.top = Math.max(0, Math.min(newTop, maxTop)) + "px"; elmnt.style.left = Math.max(0, Math.min(newLeft, maxLeft)) + "px"; } function dragEnd(e) { // Check if it was a tap (no dragging occurred) if (!isDragging && e.type === 'touchend') { const controls = elmnt.querySelector('.emotion-avatar-controls'); const slider = elmnt.querySelector('.emotion-avatar-slider-fade'); if (controls && slider) { const isVisible = controls.style.opacity === "1"; controls.style.opacity = isVisible ? "0" : "1"; slider.style.opacity = isVisible ? "0" : "1"; } } // Unbind all document-level events document.onmouseup = null; document.onmousemove = null; document.ontouchend = null; document.ontouchmove = null; // Reset the dragging flag isDragging = false; } } // 🕹️ --- Add Control Buttons (Close, Flip) --- 🕹️ function addControlButtons(container, avatar, sliderContainer) { const controls = document.createElement("div"); controls.classList.add("emotion-avatar-controls"); controls.style.opacity = "0"; sliderContainer.style.opacity = "0"; let hideTimeout; const showControls = () => { controls.style.opacity = "1"; sliderContainer.style.opacity = "1"; clearTimeout(hideTimeout); }; const hideControls = () => { controls.style.opacity = "0"; sliderContainer.style.opacity = "0"; }; const startHideTimer = () => { hideTimeout = setTimeout(hideControls, 2000); }; container.addEventListener("mouseenter", showControls); container.addEventListener("mouseleave", startHideTimer); // --- Mobile: Tap avatar to show controls, tap outside to hide --- if (!container._mobileTapHandlerAdded) { container._mobileTapHandlerAdded = true; // Show controls on avatar tap container.addEventListener("touchstart", function(e) { // Only trigger if tapping the avatar itself, not controls or slider if ( e.target === avatar || e.target === container ) { showControls(); e.stopPropagation(); } }); // Hide controls when tapping outside avatar/controls/slider document.addEventListener("touchstart", function hideOnOutsideTap(e) { if ( !container.contains(e.target) ) { hideControls(); } }); } // --- Button creation --- const closeButton = document.createElement("button"); closeButton.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;margin:auto;" class="lucide lucide-x-icon lucide-x"> <path d="M18 6 6 18"/><path d="m6 6 12 12"/> </svg> `; let closeHandled = false; const closeHandler = (e) => { e.stopPropagation(); if (e.type === "touchstart") { closeHandled = true; } container.remove(); delete botAvatars[Object.keys(botAvatars).find(key => botAvatars[key].avatar === avatar)]; scriptDisabled = true; }; closeButton.addEventListener("touchstart", function(e) { closeHandler(e); }, { passive: false }); closeButton.addEventListener("click", function(e) { if (closeHandled) { closeHandled = false; return; } closeHandler(e); }); const flipButton = document.createElement("button"); flipButton.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;margin:auto;" class="lucide lucide-arrow-left-right-icon lucide-arrow-left-right"> <path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/> </svg> `; let flipHandled = false; const flipHandler = (e) => { e.stopPropagation(); if (e.type === "touchstart") { flipHandled = true; } avatar.style.transform = avatar.style.transform === "scaleX(-1)" ? "scaleX(1)" : "scaleX(-1)"; }; flipButton.addEventListener("touchstart", function(e) { flipHandler(e); }, { passive: false }); flipButton.addEventListener("click", function(e) { if (flipHandled) { flipHandled = false; return; } flipHandler(e); }); controls.appendChild(closeButton); controls.appendChild(flipButton); return controls; } // 🔍 --- Analyze Emotion with Gemini --- 🔍 function analyzeEmotion(characterName, messageText) { if (scriptDisabled) return; /* *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* * * * SECTION 5: GEMINI PROMPT TEMPLATE * * (The prompt sent to Gemini is customizable! ✨) * * * * 📝 You can edit the prompt below to change how Gemini analyzes * * character emotions. If you add/remove emotions or change their * * descriptions/numbers in your avatar config, update the prompt list * * here to match! This helps keep the emotion detection and avatar images * * in sync for your characters and preferences. * * * *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* */ GM_log("Sending message to Gemini for emotion analysis..."); const prompt = `Analyze the following text and determine ${characterName}'s LAST DISPLAYED emotion based solely on their physical expressions, tone of voice, body language, and observable actions in the FINAL SENTENCE OR PHRASE.\n\nIgnore all internal thoughts, motivations, or non-visible context. Classify the emotion based on how a neutral observer would interpret their behavior at that moment. Focus ONLY on the visible and audible cues in the last part of the text.\n\nExample: If the character smiles warmly while gripping a knife (without visible tension), the emotion is "17" (Joy). If they cry silently with clenched fists, it's "26" (Sadness).\n\nRespond ONLY with the NUMBER corresponding to the most fitting emotion from this list, considering ONLY the LAST emotion shown:\n\n1. Admiration\n2. Amusement\n3. Anger\n4. Annoyance\n5. Approval\n6. Caring\n7. Confusion\n8. Curiosity\n9. Desire\n10. Disappointment\n11. Disapproval\n12. Disgust\n13. Embarrassment\n14. Excitement\n15. Fear\n16. Gratitude\n17. Joy\n18. Love\n19. Nervousness\n20. Neutral\n21. Optimism\n22. Pride\n23. Realization\n24. Relief\n25. Remorse\n26. Sadness\n27. Surprise\n\n${messageText}`; const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }] }; GM_xmlhttpRequest({ method: "POST", url: endpoint, headers: { "Content-Type": "application/json" }, data: JSON.stringify(payload), onload: function (res) { try { const response = JSON.parse(res.responseText); const emotionNumber = parseInt(response.candidates?.[0]?.content?.parts?.[0]?.text) || 20; addEmotionAvatar(emotionNumber, characterName); } catch (e) { console.error("Error processing response:", e.message); } }, onerror: function (err) { console.error("Error request:", err.message); }, }); } /* *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* * * * SECTION 6: JANITORAI CHAT INTEGRATION * * (Hooks into JanitorAI chat to track bot messages) * * * *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* */ // 💬 --- Chat Selectors --- 💬 const CHAT_CONTAINER_SELECTOR = '[class^="_messagesMain_"]'; const MESSAGE_CONTAINER_SELECTOR = '[data-testid="virtuoso-item-list"] > div[data-index]'; const BOT_NAME_ICON_SELECTOR = '[class^="_nameIcon_"]'; const LAST_MESSAGE_SWIPE_CONTAINER_SELECTOR = '[class^="_botChoicesContainer_"]'; const SWIPE_SLIDER_SELECTOR = '[class^="_botChoicesSlider_"]'; const MESSAGE_WRAPPER_SELECTOR = 'li[class^="_messageDisplayWrapper_"]'; const MESSAGE_TEXT_SELECTOR = ".css-ji4crq p"; const EDIT_PANEL_SELECTOR = '[class^="_editPanel_"]'; const CONTROL_PANEL_SELECTOR = '[class^="_controlPanel_"]'; const BOT_NAME_SELECTOR = '[class^="_nameText_"]'; let lastLoggedText = ""; let lastLoggedStatus = ""; let lastLoggedSwipeIndex = -1; let lastLoggedMessageIndex = -1; // 📝 --- Log Message Status --- 📝 /** * Finds the last bot message, determines its state (swipe, text, status), * and logs the information. */ function logMessageStatus() { if (scriptDisabled) return; // <--- Early return if disabled const allMessageNodes = document.querySelectorAll(MESSAGE_CONTAINER_SELECTOR); if (allMessageNodes.length === 0) return; // Find the last message from a bot let lastBotMessageContainer = null; for (let i = allMessageNodes.length - 1; i >= 0; i--) { if (allMessageNodes[i].querySelector(BOT_NAME_ICON_SELECTOR)) { lastBotMessageContainer = allMessageNodes[i]; break; } } if (!lastBotMessageContainer) return; // No bot messages found const botNameElement = lastBotMessageContainer.querySelector(BOT_NAME_SELECTOR); const characterName = botNameElement ? botNameElement.textContent.trim() : null; const messageIndex = parseInt(lastBotMessageContainer.dataset.index, 10); let activeMessageNode; let activeSwipeIndex = 0; // Default to 0 for non-swipeable messages // Check if this is the swipeable container or a regular message const swipeContainer = lastBotMessageContainer.querySelector(LAST_MESSAGE_SWIPE_CONTAINER_SELECTOR); if (swipeContainer) { // It's the last message with swipes const slider = swipeContainer.querySelector(SWIPE_SLIDER_SELECTOR); if (!slider) return; const transform = slider.style.transform; const translateX = transform ? parseFloat(transform.match(/translateX\(([-0-9.]+)%\)/)?.[1] || "0") : 0; activeSwipeIndex = Math.round(Math.abs(translateX) / 100); const allSwipes = slider.querySelectorAll(MESSAGE_WRAPPER_SELECTOR); if (allSwipes.length <= activeSwipeIndex) return; activeMessageNode = allSwipes[activeSwipeIndex]; } else { // It's an older, non-swipeable message activeMessageNode = lastBotMessageContainer.querySelector(MESSAGE_WRAPPER_SELECTOR); } if (!activeMessageNode) return; const textElements = activeMessageNode.querySelectorAll(MESSAGE_TEXT_SELECTOR); const messageText = textElements.length > 0 ? Array.from(textElements) .map((p) => p.textContent.trim()) .join("\n") : "[No text found]"; const isEditing = !!activeMessageNode.querySelector(EDIT_PANEL_SELECTOR); const isFinished = !!activeMessageNode.querySelector(CONTROL_PANEL_SELECTOR); let status; if (isEditing) { status = "Editing"; } else if (isFinished) { status = "Finished"; } else { status = "Streaming"; } const shouldLog = status !== lastLoggedStatus || activeSwipeIndex !== lastLoggedSwipeIndex || messageIndex !== lastLoggedMessageIndex || (status !== "Streaming" && messageText !== lastLoggedText); if (shouldLog) { GM_log(`Message Index: ${messageIndex} (Swipe ${activeSwipeIndex + 1}):`); if (status === "Streaming") { GM_log(`Status: ${status}`); } else { GM_log(`Text: "${messageText}"`); GM_log(`Status: ${status}`); if (status === "Finished" && messageText !== "[No text found]" && characterName) { analyzeEmotion(characterName, messageText); } } GM_log("--------------------"); lastLoggedStatus = status; lastLoggedSwipeIndex = activeSwipeIndex; lastLoggedMessageIndex = messageIndex; if (status !== "Streaming") { lastLoggedText = messageText; } else { lastLoggedText = ""; } } } // 👀 --- Initialize Observer --- 👀 /** * Initializes the observer to watch for chat changes. */ function initializeObserver() { if (scriptDisabled) return; const container = document.querySelector(CHAT_CONTAINER_SELECTOR); if (container) { const observer = new MutationObserver((mutations) => { logMessageStatus(); }); observer.observe(container, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'], }); // Initial check logMessageStatus(); } else { setTimeout(initializeObserver, 1000); } } // 🚀 --- Script Start --- 🚀 if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeObserver); } else { initializeObserver(); } /* *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* * * * SECTION 7: UTILITY FUNCTIONS * * (Helpers for image sizing and viewport calculations) * * * *・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.*.。.:*・☆・゜・*:.。.:*・* */ // 📏 --- Get Visible Area --- 📏 function getVisibleArea() { if (scriptDisabled) return; return { width: window.innerWidth, height: window.innerHeight }; } // 🖼️ --- Get Image Natural Size --- 🖼️ function getImageNaturalSize(img, callback) { if (scriptDisabled) return; if (img.complete) { callback({ width: img.naturalWidth, height: img.naturalHeight }); } else { img.onload = () => { callback({ width: img.naturalWidth, height: img.naturalHeight }); }; } } // 📐 --- Get Max Avatar Size --- 📐 function getMaxAvatarSize(img, callback) { if (scriptDisabled) return; getImageNaturalSize(img, ({ width: imgW, height: imgH }) => { const { width: vpW, height: vpH } = getVisibleArea(); const maxW = vpW - 50; const maxH = vpH - 50; // Maintain aspect ratio const aspect = imgW / imgH; let finalW = maxW, finalH = maxW / aspect; if (finalH > maxH) { finalH = maxH; finalW = maxH * aspect; } callback({ width: Math.floor(finalW), height: Math.floor(finalH) }); }); } })();