Skip to content

Instantly share code, notes, and snippets.

@rexxar31
Last active December 5, 2025 00:15
Show Gist options
  • Select an option

  • Save rexxar31/98436c16401b60af4e0b2723c72071de to your computer and use it in GitHub Desktop.

Select an option

Save rexxar31/98436c16401b60af4e0b2723c72071de to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Chat Script
// @namespace test
// @version 2.2.6
// @description For CW use only
// @match https://agents.moderationinterface.com/*
// @require https://cdn.socket.io/4.7.5/socket.io.min.js
// @updateURL https://gist.githubusercontent.com/rexxar31/98436c16401b60af4e0b2723c72071de/raw/prodFront_final.user.js
// @grant window.focus
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
;(function () {
'use strict'
// --- CONFIGURATION ---
const CONFIG = {
ui: {
imageSelector: 'img.img-thumbnail[src*="cache.moderationinterface.com"]',
blurAmount: '20px',
hoverText: 'Hover to reveal',
// Element Selectors (IDs are faster than classes)
textareaSelector: '#chat-windows-message-textarea',
sessionSelector: 'app-session-detail span.badge.badge-secondary',
timelineSelector: '.timeline',
},
server: {
url: 'http://localhost:5000',
},
}
// --- UTILITIES ---
// Prevents function from firing too often (Performance Optimization)
const debounce = (func, wait) => {
let timeout
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, args), wait)
}
}
// Efficiently waits for an element to appear in the DOM
const waitForElement = (selector, timeout = 10000) => {
return new Promise((resolve, reject) => {
const elem = document.querySelector(selector)
if (elem) return resolve(elem)
const observer = new MutationObserver(() => {
const elem = document.querySelector(selector)
if (elem) {
observer.disconnect()
resolve(elem)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
reject(new Error(`Timeout waiting for ${selector}`))
}, timeout)
})
}
// Fixed: Robust value setter that mimics user typing
// This is critical for Angular validation to detect the change and enable the button
const setNativeValue = (element, value) => {
const valueDescriptor = Object.getOwnPropertyDescriptor(element, 'value')
const valueSetter = valueDescriptor ? valueDescriptor.set : null
const prototype = Object.getPrototypeOf(element)
const prototypeValueDescriptor = Object.getOwnPropertyDescriptor(
prototype,
'value',
)
const prototypeValueSetter = prototypeValueDescriptor
? prototypeValueDescriptor.set
: null
if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value)
} else if (valueSetter) {
valueSetter.call(element, value)
} else {
element.value = value
}
element.dispatchEvent(new Event('input', { bubbles: true }))
element.dispatchEvent(new Event('change', { bubbles: true }))
}
// --- STYLES ---
const STYLES = `
:root { --blur-amount: ${CONFIG.ui.blurAmount}; }
.blurred-image { filter: blur(var(--blur-amount)); transition: filter 0.3s ease; }
.blurred-image:hover { filter: blur(0); }
.image-wrapper { position: relative; display: inline-block; }
.hover-hint {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7); color: white; padding: 4px 8px;
border-radius: 4px; pointer-events: none; transition: opacity 0.3s;
}
.image-wrapper:hover .hover-hint { opacity: 0; }
.control-panel {
position: fixed; top: 10px; left: 150px; z-index: 9999;
background: white; padding: 8px 12px; border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); font-family: sans-serif;
display: flex; align-items: center; gap: 8px;
}
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #d93025; }
.status-dot.connected { background: #2ecc71; }
.notify-banner {
position: fixed; top: 60px; left: 50%; transform: translateX(-50%);
background: #4CAF50; color: white; padding: 10px 20px;
border-radius: 4px; z-index: 10000; animation: fadeOut 3s forwards;
}
@keyframes fadeOut { 0% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
`
// --- UI MANAGER ---
class UIManager {
constructor() {
this.browserIdentifier = this.getOrSetBrowserId()
GM_addStyle(STYLES)
this.renderControlPanel()
this.debouncedBlur = debounce(this.applyImageBlur.bind(this), 200)
this.observeDOM()
}
getOrSetBrowserId() {
let id = GM_getValue('browserId')
if (!id) {
id = `browser_${Math.random().toString(36).substr(2, 9)}`
GM_setValue('browserId', id)
}
return id
}
renderControlPanel() {
const panel = document.createElement('div')
panel.className = 'control-panel'
panel.innerHTML = `
<span style="font-size: 12px; font-weight: bold; text-transform: uppercase;">Status:</span>
<div id="socket-status" class="status-dot"></div>
<span id="socket-text" style="font-size: 13px;">Disconnected</span>
`
document.body.appendChild(panel)
this.statusDot = panel.querySelector('#socket-status')
this.statusText = panel.querySelector('#socket-text')
}
setConnectionStatus(isConnected) {
if (!this.statusDot) return
this.statusDot.classList.toggle('connected', isConnected)
this.statusText.textContent = isConnected ? 'Connected' : 'Disconnected'
}
showNotification(msg) {
const el = document.createElement('div')
el.className = 'notify-banner'
el.textContent = msg
document.body.appendChild(el)
setTimeout(() => el.remove(), 3000)
}
applyImageBlur() {
const images = document.querySelectorAll(CONFIG.ui.imageSelector)
images.forEach((img) => {
if (img.closest('.image-wrapper')) return
const wrapper = document.createElement('div')
wrapper.className = 'image-wrapper'
const hint = document.createElement('div')
hint.className = 'hover-hint'
hint.textContent = CONFIG.ui.hoverText
img.classList.add('blurred-image')
img.parentNode.insertBefore(wrapper, img)
wrapper.appendChild(img)
wrapper.appendChild(hint)
})
}
observeDOM() {
const observer = new MutationObserver((mutations) => {
const nodesAdded = mutations.some((m) => m.addedNodes.length > 0)
if (nodesAdded) this.debouncedBlur()
})
observer.observe(document.body, { childList: true, subtree: true })
}
}
// --- MESSAGE MANAGER ---
class MessageManager {
getAgentEnNumber() {
const el = document.querySelector('.nav-link span.ng-star-inserted')
return el?.textContent?.match(/\(([^)]+)\)/)?.[1] || 'unknown'
}
getMessages() {
const panels = Array.from(
document.querySelectorAll(
'app-receive-message-item, app-sent-message-item',
),
)
// CORRECT LOGIC:
// The DOM usually has the NEWEST messages at the top (index 0).
// We want the first 10 elements (the most recent conversation history).
const relevantPanels = panels.slice(0, 5)
const messages = relevantPanels.reduce((acc, panel) => {
const textEl = panel.querySelector('.timeline-body p')
const text = textEl?.textContent?.trim() || ''
if (!text || text.includes('ALERT: Low Balance')) return acc
const isAgentIcon =
panel
.querySelector(
'.timeline-badge.warning .material-icons, .timeline-badge.success .material-icons',
)
?.textContent?.trim() === 'extension'
const isClient =
panel.tagName.toLowerCase() === 'app-receive-message-item'
const isAuto = text.startsWith('Automated text:')
let sender = 'agent'
if (isClient && !isAgentIcon) sender = 'client'
if (isAuto) sender = 'agent'
acc.push({
text: isAuto ? text.replace('Automated text:', '').trim() : text,
sender,
})
return acc
}, [])
// CORRECT LOGIC:
// The 'messages' array is currently [Newest -> Oldest].
// The AI/Backend expects chronological order [Oldest -> Newest].
// So we reverse it here.
return messages.reverse()
}
getProfileInfo() {
const getVal = (id) => document.getElementById(id)?.value?.trim() || ''
return {
location: getVal('moderator-city') || 'unknown',
name: getVal('moderator-name') || 'moderator',
age: getVal('moderator-age') || 'unspecified',
customerAge: getVal('customer-age') || 'unspecified',
gender: this.detectGender(),
countryCode: this.getCountryCode(),
customerCustom: getVal('customer-custom'),
moderatorCustom: getVal('moderator-custom'),
customerDescription: getVal('customer-description'),
moderatorDescription: getVal('moderator-description'),
}
}
detectGender() {
const txt =
document
.querySelector('.alert.alert-light p')
?.textContent?.toLowerCase() || ''
if (txt.includes('gay')) return 'gay'
if (txt.includes('tran')) return 'trans'
return 'woman'
}
getCountryCode() {
const text =
document.querySelector('.alert.alert-light p')?.textContent || ''
if (!text) return null
const match = text.match(/\[?([A-Z]{2})[-_]EN\]?/)
if (match) return match[1] === 'IR' ? 'IE' : match[1]
if (text.includes('USA-EN')) return 'US'
return null
}
}
// --- CHAT ASSISTANT (CONTROLLER) ---
class ChatAssistant {
constructor() {
this.ui = new UIManager()
this.msg = new MessageManager()
this.sessionId = null
this.socket = null
this.lastFingerprint = ''
this.initSocket()
this.initSessionObserver()
}
initSocket() {
this.socket = io(CONFIG.server.url, {
query: { browserId: this.ui.browserIdentifier },
reconnectionDelay: 1000,
})
this.socket.on('connect', () => {
console.log('[ChatAssistant] Connected')
this.ui.setConnectionStatus(true)
})
this.socket.on('disconnect', () => this.ui.setConnectionStatus(false))
this.socket.on('session_updated', (data) => {
// This is the event triggered when you press "Send" on the Python GUI
if (data.sessionId === this.sessionId && data.action === 'send') {
if (data.profileInfo) this.fillProfileFields(data.profileInfo)
this.automateResponse(data.response)
}
})
}
initSessionObserver() {
const checkSession = () => {
const el = document.querySelector(CONFIG.ui.sessionSelector)
const newId = el ? el.textContent.split('#')[1]?.trim() : null
if (newId !== this.sessionId) {
console.log(
`[ChatAssistant] Session changed: ${this.sessionId} -> ${newId}`,
)
if (this.sessionId && !newId) {
this.socket.emit('update_session', {
action: 'remove',
browserId: this.ui.browserIdentifier,
sessionId: this.sessionId,
})
this.lastFingerprint = ''
}
this.sessionId = newId
if (newId) {
const area = document.querySelector(CONFIG.ui.textareaSelector)
if (area) area.value = ''
this.handleNewSession()
this.initMessageSync()
}
}
}
const observer = new MutationObserver(debounce(checkSession, 50))
observer.observe(document.body, { childList: true, subtree: true })
checkSession()
}
initMessageSync() {
waitForElement(CONFIG.ui.timelineSelector)
.then((el) => {
console.log('[ChatAssistant] Timeline found, attaching observer.')
const observer = new MutationObserver(
debounce(() => {
if (this.sessionId) this.syncSessionData()
}, 100),
)
observer.observe(el, { childList: true, subtree: true })
})
.catch((e) => {})
}
async handleNewSession() {
this.syncSessionData('submit_session')
}
async syncSessionData(eventName = 'update_session') {
if (!this.sessionId) return
const messages = this.msg.getMessages()
const currentFingerprint = messages
.map((m) => m.text + m.sender)
.join('|')
// Don't resend if nothing changed (Bandwidth Optimization)
if (
currentFingerprint === this.lastFingerprint &&
eventName === 'update_session'
) {
return
}
this.lastFingerprint = currentFingerprint
const data = {
browserId: this.ui.browserIdentifier,
sessionId: this.sessionId,
messages: messages,
profileInfo: this.msg.getProfileInfo(),
agentEnNumber: this.msg.getAgentEnNumber(),
serviceText:
document.querySelector('.alert.alert-light p')?.textContent?.trim() ||
'',
customerRating: this.getCustomerRating(),
}
this.socket.emit(eventName, data)
}
getCustomerRating() {
const ratingBadge = document.querySelector(
'.card-footer bar-rating + .badge',
)
return ratingBadge ? parseInt(ratingBadge.textContent.trim(), 10) || 0 : 0
}
fillProfileFields(info) {
const mapping = {
'customer-custom': info.customerCustom,
'customer-description': info.customerDescription,
'moderator-custom': info.moderatorCustom,
'moderator-description': info.moderatorDescription,
}
Object.entries(mapping).forEach(([id, val]) => {
if (val === undefined) return
const el = document.getElementById(id)
if (el && el.value !== val) setNativeValue(el, val)
})
}
async automateResponse(text) {
try {
const textarea = await waitForElement(CONFIG.ui.textareaSelector)
// 1. Set value using the fixed robust setter
setNativeValue(textarea, text + ' ')
// 2. Extra event triggers to force Angular validation
textarea.dispatchEvent(new Event('input', { bubbles: true }))
// This keyup event is often required by older Angular apps to mark the field "dirty"
textarea.dispatchEvent(
new KeyboardEvent('keyup', { bubbles: true, key: ' ' }),
)
textarea.focus()
// 3. SAFETY DELAY: Wait 500ms for Angular validation to run and render any alerts
// This prevents clicking "Send" before the "Duplicate Message" error appears.
setTimeout(() => {
this.waitForSafetyAndSubmit()
}, 500)
} catch (err) {
this.ui.showNotification('Could not insert text: ' + err.message)
console.error(err)
}
}
waitForSafetyAndSubmit(attempts = 0) {
// 1. Check for Errors FIRST
// We check this immediately. If the alert is there, we ABORT.
const errorAlert = document.querySelector('.alert.alert-danger')
if (errorAlert) {
this.ui.showNotification(
'Error/Duplicate detected! Aborting auto-submit.',
)
return
}
// 2. Check Button State
const btn = document.querySelector('button[type="submit"].btn-primary')
if (btn && !btn.disabled) {
btn.click()
this.ui.showNotification('Auto-submitted!')
return
}
// 3. Keep Waiting
if (attempts < 20) {
setTimeout(() => this.waitForSafetyAndSubmit(attempts + 1), 250)
} else {
this.ui.showNotification(
'Submit button stuck disabled (Validation failed?)',
)
}
}
}
const assistant = new ChatAssistant()
window.chatAssistant = assistant
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment