Skip to content

Instantly share code, notes, and snippets.

@rexxar31
Last active September 22, 2025 21:28
Show Gist options
  • Select an option

  • Save rexxar31/450ea0e10632a0677b4e9189ab89eb3f to your computer and use it in GitHub Desktop.

Select an option

Save rexxar31/450ea0e10632a0677b4e9189ab89eb3f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Chat Assistant Test
// @namespace test
// @version 1.0.8
// @description Chat Assistant Front-End Interface
// @match https://agents.moderationinterface.com/*
// @require https://cdn.socket.io/4.7.5/socket.io.min.js
// @updateURL https://gist.github.com/rexxar31/450ea0e10632a0677b4e9189ab89eb3f/raw/251a960052b5c4867944799d05eb97d76582bf87/ChatAssistant_duplicatecheck.user.js
// @grant window.focus
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
;(function () {
'use strict'
// Constants
const CONFIG = {
ui: {
imageSelector: 'img.img-thumbnail[src*="cache.moderationinterface.com"]',
blurAmount: '20px',
hoverText: 'Hover to reveal',
sessionSelector:
'span.badge.badge-secondary.float-right.ng-star-inserted',
textareaSelector: '#chat-windows-message-textarea',
},
server: {
url: 'http://localhost:5000', // Central hub URL
},
reactivationPhrases: [
'[please reactivate the user!]',
'[Bitte User reaktivieren!]',
],
}
// Styles
const STYLES = {
imageBlur: `
:root {
--blur-amount: ${CONFIG.ui.blurAmount};
--transition-speed: 0.3s;
--hover-bg: rgba(0, 0, 0, 0.7);
}
.blurred-image {
filter: blur(var(--blur-amount));
transition: filter var(--transition-speed) ease-in-out;
}
.blurred-image:hover {
filter: blur(0);
}
.image-container {
position: relative;
display: inline-block;
}
.hover-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--hover-bg);
color: white;
padding: 5px 10px;
border-radius: 5px;
pointer-events: none;
opacity: 1;
transition: opacity var(--transition-speed) ease-in-out;
}
.image-container:hover .hover-text {
opacity: 0;
}`,
notification: `
.notification-banner {
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
background-color: #4CAF50;
color: white;
padding: 12px 24px;
border-radius: 4px;
z-index: 10000;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
animation: slideDown 0.5s ease-out, fadeOut 0.5s ease-in 2.5s forwards;
}`,
controls: `
.control-panel {
position: fixed;
top: 10px;
left: 150px;
z-index: 9999;
display: flex;
gap: 10px;
align-items: center;
background: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}`,
}
class UIManager {
constructor(chatAssistant) {
this.chatAssistant = chatAssistant
this.browserIdentifier = this.generateBrowserId()
this.initializeStyles()
this.initializeUI()
this.applyImageBlur()
this.observeImageChanges()
}
updateStatus(message) {
this.statusTextbox.value = message
}
generateBrowserId() {
let browserId = GM_getValue('browserId')
if (!browserId) {
browserId = 'browser_' + Math.random().toString(36).substr(2, 9)
GM_setValue('browserId', browserId)
}
return browserId
}
initializeStyles() {
GM_addStyle(STYLES.imageBlur)
GM_addStyle(STYLES.notification)
GM_addStyle(STYLES.controls)
}
initializeUI() {
const controlPanel = document.createElement('div')
controlPanel.className = 'control-panel'
const apiSelect = this.createAPISelect()
const regenerateButton = this.createButton('Regenerate', () => {
this.chatAssistant.handleRegenerateClick()
})
this.statusTextbox = document.createElement('input')
this.statusTextbox.type = 'text'
this.statusTextbox.className = 'status-textbox'
this.statusTextbox.value = 'Waiting'
this.statusTextbox.readOnly = true
controlPanel.appendChild(apiSelect)
controlPanel.appendChild(regenerateButton)
controlPanel.appendChild(this.statusTextbox)
document.body.appendChild(controlPanel)
}
createAPISelect() {
const select = document.createElement('select')
select.className = 'api-select'
const options = [
{ value: 'or-llama3.3', text: 'OR - Llama 3.3-70b' },
{ value: 'or-llama4-maverick', text: 'OR - Llama 4 Maverick' },
{ value: 'voidai-llama', text: 'Void - Llama 3.3-70B' },
{ value: 'voidai-deepseekv3', text: 'Void - Deepseek V3' },
{ value: 'zanity', text: 'Zanity - L3.3-70b' },
{ value: 'zanity-l3.1-405b', text: 'Zanity - L3.1-401b' },
{ value: 'deepInfra', text: 'DeepInfra' },
{ value: 'klusterai', text: 'KlusterAI - LLama 3.3' },
]
options.forEach((opt) => {
const option = document.createElement('option')
option.value = opt.value
option.textContent = opt.text
select.appendChild(option)
})
select.value = GM_getValue('selectedAPI')
select.addEventListener('change', () => {
GM_setValue('selectedAPI', select.value)
this.showNotification(
`Switched to ${select.options[select.selectedIndex].text} API`,
)
})
return select
}
createButton(text, onClick) {
const button = document.createElement('button')
button.textContent = text
button.className = 'control-button'
button.addEventListener('click', onClick)
return button
}
applyImageBlur() {
const images = document.querySelectorAll(CONFIG.ui.imageSelector)
images.forEach((img) => {
if (img.closest('.image-container')) return
const container = document.createElement('div')
container.className = 'image-container'
const hoverText = document.createElement('div')
hoverText.className = 'hover-text'
hoverText.textContent = CONFIG.ui.hoverText
img.classList.add('blurred-image')
img.parentNode.insertBefore(container, img)
container.appendChild(img)
container.appendChild(hoverText)
})
}
observeImageChanges() {
new MutationObserver(() => this.applyImageBlur()).observe(document.body, {
childList: true,
subtree: true,
})
}
showNotification(message) {
const notification = document.createElement('div')
notification.className = 'notification-banner'
notification.textContent = message
document.body.appendChild(notification)
setTimeout(() => notification.remove(), 3000)
}
}
class MessageManager {
constructor() {
this.currentSessionId = null
this.agentEnNumber = null
this.lastMessageFingerprint = ''
this.initMessageObserver()
}
getSubmitButton() {
return document.querySelector('button.btn.btn-primary[type="submit"]')
}
getAgentEnNumber() {
if (this.agentEnNumber) return this.agentEnNumber
const enNumberElement = document.querySelector(
'.nav-link .ng-star-inserted',
)
this.agentEnNumber =
enNumberElement?.textContent?.match(/\(([^)]+)\)/)?.[1] || 'unknown'
return this.agentEnNumber
}
isAgentLikeMessage(panel) {
const extensionIcon = panel.querySelector(
'.timeline-badge.warning .material-icons, .timeline-badge.success .material-icons',
)
return extensionIcon && extensionIcon.textContent.trim() === 'extension'
}
getMessages() {
const panels = document.querySelectorAll(
'app-receive-message-item, app-sent-message-item',
)
const messages = Array.from(panels)
.slice(0, 5)
.reduce((acc, panel) => {
const messageBody = panel.querySelector('.timeline-body p')
const messageText = messageBody?.textContent.trim() ?? ''
// NEW: Skip messages containing "ALERT: Low Balance"
if (messageText.includes('ALERT: Low Balance')) {
return acc
}
const isClientMessage =
panel.tagName.toLowerCase() === 'app-receive-message-item'
const isAgentLikeMessage = this.isAgentLikeMessage(panel)
const sender = isAgentLikeMessage
? 'agent'
: isClientMessage
? 'client'
: 'agent'
const isAutomated = messageText.startsWith('Automated text:')
acc.push({
text: isAutomated ? messageText.slice(15) : messageText,
sender: isAutomated ? 'agent' : sender,
})
return acc
}, [])
return messages.reverse()
}
getProfileInfo() {
return {
location:
document.getElementById('moderator-city')?.value?.trim() || 'unknown',
name:
document.getElementById('moderator-name')?.value?.trim() ||
'moderator',
age:
document.getElementById('moderator-age')?.value?.trim() ||
'unspecified',
customerAge:
document.getElementById('customer-age')?.value?.trim() ||
'unspecified',
gender: this.detectGender(),
countryCode: this.getCountryCode(),
customerCustom:
document.getElementById('customer-custom')?.value?.trim() || '',
moderatorCustom:
document.getElementById('moderator-custom')?.value?.trim() || '',
customerDescription:
document.getElementById('customer-description')?.value?.trim() || '',
moderatorDescription:
document.getElementById('moderator-description')?.value?.trim() || '',
}
}
detectGender() {
const serviceElement = document.querySelector('.alert.alert-light p')
if (!serviceElement) return 'woman'
const serviceText = serviceElement.textContent.toLowerCase()
if (serviceText.includes('gay')) return 'gay'
if (serviceText.includes('tran')) return 'trans'
return 'woman'
}
initMessageObserver() {
const target = document.body
const observer = new MutationObserver(() => {
const messages = this.getMessages()
const newFingerprint = messages.map((m) => m.text).join('|')
if (newFingerprint !== this.lastMessageFingerprint) {
this.lastMessageFingerprint = newFingerprint
this.chatAssistant.handleMessagesUpdate()
}
})
observer.observe(target, { childList: true, subtree: true })
}
getCountryCode() {
const element = document.querySelector('.alert.alert-light p')
if (!element) return null
const text = element.textContent
if (text.includes('USA-EN')) return 'US'
let match = text.match(/([A-Z]{2})-EN/)
if (match) {
const code = match[1]
const lowerText = text.toLowerCase()
switch (code) {
case 'SA':
return lowerText.includes('saudi arabia') ? 'SA' : 'US'
case 'IR':
return 'IE'
default:
return code
}
}
match = text.match(/\[([A-Z]{2})_EN\]/)
if (match) return match[1]
match = text.match(/\b([A-Z]{2})\b/)
if (match) return match[1]
return null
}
}
class ChatAssistant {
constructor() {
this.uiManager = new UIManager(this)
this.messageManager = new MessageManager()
this.currentSessionId = null
this.socket = null
this.initializeSocket()
this.initializeObserver()
}
initializeSocket() {
this.socket = io(CONFIG.server.url, {
query: {
browserId: this.uiManager.browserIdentifier,
},
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
})
this.socket.on('connect', () => {
console.log('Connected to WebSocket server')
this.uiManager.showNotification('Connected to Hub')
})
this.socket.on('session_updated', (data) => {
if (
data.sessionId === this.currentSessionId &&
data.action === 'send'
) {
this.waitForTextareaAndInsert(
data.response,
this.uiManager.browserIdentifier,
)
}
})
this.socket.on('disconnect', () => {
console.log('Disconnected from WebSocket server')
this.uiManager.showNotification('Disconnected from Hub')
})
}
initializeObserver() {
const observer = new MutationObserver(() => this.checkForNewAssignment())
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
})
this.checkForNewAssignment()
}
async checkForNewAssignment() {
const sessionSpan = document.querySelector(CONFIG.ui.sessionSelector)
if (!sessionSpan || sessionSpan.style.display === 'none') {
this.currentSessionId = null
this.uiManager.updateStatus('Waiting')
return
}
const newSessionId = sessionSpan.textContent.split('#')[1]?.trim()
if (newSessionId && newSessionId !== this.currentSessionId) {
this.currentSessionId = newSessionId
document.querySelector(CONFIG.ui.textareaSelector).value = ''
await this.handleNewAssignment()
}
}
async handleNewAssignment() {
this.uiManager.updateStatus('Request Sent')
const messages = this.messageManager.getMessages()
const profileInfo = this.messageManager.getProfileInfo()
const agentEnNumber = this.messageManager.getAgentEnNumber()
const apiType = GM_getValue('selectedAPI')
// Get the service element text
const serviceElement = document.querySelector('.alert.alert-light p')
const serviceText = serviceElement
? serviceElement.textContent.trim()
: ''
const requestData = {
browserId: this.uiManager.browserIdentifier,
sessionId: this.currentSessionId,
messages,
profileInfo,
selectedAPI: apiType,
agentEnNumber: this.messageManager.agentEnNumber,
serviceText,
}
try {
await this.sendToServer(requestData)
this.uiManager.updateStatus('Waiting for hub...')
} catch (error) {
console.error('Server communication error:', error)
this.uiManager.showNotification('Failed to queue request')
}
}
async sendToServer(data) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'POST',
url: CONFIG.server.url + '/submit',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(data),
onload: () => resolve(true),
onerror: () => resolve(false),
})
})
}
async sendUpdateToServer(data) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: CONFIG.server.url + '/update',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(data),
onload: () => resolve(true),
onerror: () => reject(false),
})
})
}
async handleRegenerateClick() {
this.uiManager.updateStatus('Request Sent')
const messages = this.messageManager.getMessages()
const profileInfo = this.messageManager.getProfileInfo()
const serviceElement = document.querySelector('.alert.alert-light p')
const serviceText = serviceElement
? serviceElement.textContent.trim()
: ''
const agentEnNumber = this.messageManager.getAgentEnNumber()
const requestData = {
browserId: this.uiManager.browserIdentifier,
sessionId: this.currentSessionId,
messages,
profileInfo,
selectedAPI: GM_getValue('selectedAPI'),
agentEnNumber: agentEnNumber,
serviceText: serviceText,
}
try {
await this.sendUpdateToServer(requestData)
this.uiManager.updateStatus('Regeneration queued')
} catch (error) {
this.uiManager.showNotification('Failed to queue regeneration')
}
}
waitForTextareaAndInsert(reply, browserId) {
const maxAttempts = 100
let attempts = 0
const checkInterval = setInterval(() => {
const textarea = document.querySelector(CONFIG.ui.textareaSelector)
if (textarea) {
clearInterval(checkInterval)
textarea.value = reply + ' '
textarea.dispatchEvent(new Event('input', { bubbles: true }))
textarea.setSelectionRange(
textarea.value.length,
textarea.value.length,
)
textarea.dispatchEvent(
new KeyboardEvent('keyup', { bubbles: true, key: ' ' }),
)
textarea.focus()
// Add 1-second delay before checking for duplicates and button state
setTimeout(() => {
const duplicateAlert = document.querySelector(
'.alert.alert-danger.ng-star-inserted',
)
if (duplicateAlert) {
this.uiManager.showNotification(
'Duplicate detected! Please modify message and submit manually',
)
return
}
// Start checking for enabled button state
let buttonCheckAttempts = 0
const maxButtonChecks = 20 // 5 seconds total (250ms * 20)
const checkButtonState = () => {
const submitButton = this.messageManager.getSubmitButton()
if (submitButton && !submitButton.disabled) {
submitButton.click()
this.uiManager.showNotification('Auto-submitted response!')
return
}
if (buttonCheckAttempts >= maxButtonChecks) {
this.uiManager.showNotification(
'Submit button not enabled after 5s - please submit manually',
)
return
}
buttonCheckAttempts++
setTimeout(checkButtonState, 250)
}
checkButtonState()
}, 1000)
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval)
this.uiManager.showNotification('Failed to find textarea')
}
attempts++
}, 100)
}
handleMessagesUpdate() {
if (!this.currentSessionId) return
const messages = this.messageManager.getMessages()
const profileInfo = this.messageManager.getProfileInfo()
const serviceElement = document.querySelector('.alert.alert-light p')
const serviceText = serviceElement
? serviceElement.textContent.trim()
: ''
const requestData = {
browserId: this.uiManager.browserIdentifier,
sessionId: this.currentSessionId,
messages,
profileInfo,
selectedAPI: GM_getValue('selectedAPI'),
agentEnNumber: this.messageManager.getAgentEnNumber(),
serviceText,
}
this.sendUpdateToServer(requestData)
.then(() =>
this.uiManager.showNotification('Session updated with new messages'),
)
.catch((error) => console.error('Update error:', error))
}
}
const chatAssistant = new ChatAssistant()
window.chatAssistant = chatAssistant
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment