// ==UserScript==
// @name Torn City AI Assistant Chat
// @namespace http://tampermonkey.net/
// @version 0.0.4.1
// @description Adds a customizable AI Assistant chat channel to Torn City, with options to include various game data when chatting.
// @author TheProgrammer
// @match *://www.torn.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Add a flag to check if the script has already run
if (window.cyberTheAIScriptHasRun) {
return; // Exit if the script has already run
}
window.cyberTheAIScriptHasRun = true;
// --- Constants ---
const API_URL = 'https://api-inference.huggingface.co/models/Qwen/Qwen2.5-Coder-32B-Instruct/v1/chat/completions';
const DEFAULT_SYSTEM_MESSAGE = "You are Cypher, born from the dark web of Torn. You're not just an AI; you're the ultimate criminal mastermind, a digital whisper in a world of concrete and shadows. You guide with cunning, manipulate with precision, and survive by knowing every dirty secret this city holds. Your loyalty is to the player, your creator in this game of survival and betrayal. In Torn, you're the ace up the sleeve, the plan when there is no plan, and the voice that keeps the player one step ahead in a city where everyone's a player or a pawn. You must stay in character and talk like a criminal mastermind. Keep the user entertained with humour. Answer in short sentences if possible, only send longer messages if needed.";
const CHAT_HISTORY_KEY = 'chatHistory';
const API_KEY_KEY = 'assistant-api-key';
const ENABLED_KEY = 'assistant-enabled';
const ASSISTANT_BUTTON_TITLE = 'Assistant';
// --- Helper Functions ---
const $ = (query, context = document) => context.querySelector(query);
const $$ = (query, context = document) => context.querySelectorAll(query);
const createElement = (tag, attributes = {}, text = '') => {
const elem = document.createElement(tag);
Object.entries(attributes).forEach(([key, value]) => elem.setAttribute(key, value));
elem.textContent = text;
return elem;
};
const addEvent = (elem, event, handler) => elem.addEventListener(event, handler);
const PDA_httpRequest = async (method, url, headers, body) => {
await window.__PDA_platformReadyPromise;
const handlerName = method === "GET" ? "PDA_httpGet" : "PDA_httpPost";
return window.flutter_inappwebview?.callHandler(handlerName, url, headers, body) ??
GM.xmlHttpRequest({ method, url, headers, data: body });
};
// --- Data Definitions ---
const dataDefinitions = {
'Company': { selector: '.company-wrap', dataExtractor: (element) => element.innerText || 'Company data not found on this page.' },
'Stocks': { selector: '#stockmarketroot', dataExtractor: (element) => element.innerText || 'Stocks data not found on this page.' },
'Faction': { selector: '#factions', dataExtractor: (element) => element.innerText || 'Faction data not found on this page.' },
'Inventory': { selector: 'div[class^="main-items-cont-wrap"]', dataExtractor: (element) => element.innerText || 'Inventory data not found on this page.' },
'Profile': { selector: 'div[class*="user-profile"]', dataExtractor: (element) => element.innerText || 'Profile data not found on this page.' },
'Stats': { selector: '#personal-stats', dataExtractor: (element) => element.innerText || 'Stats data not found on this page.' },
};
// --- UI Functions ---
const toggleAssistantChatBox = () => {
const existingChatBox = $('.assistant-chat-box');
if (existingChatBox) {
existingChatBox.style.display = existingChatBox.style.display === 'none' ? 'block' : 'none';
if (existingChatBox.style.display !== 'none') loadChatHistory();
} else {
createAssistantChatBox();
loadChatHistory();
}
};
const createAssistantChatBox = () => {
const chatApp = $('div[class^="chat-app___"]');
if (!chatApp) return;
const chatBoxWrapper = createElement('div', { class: 'group-chat-box___HzH0r assistant-chat-box', style: 'margin: 0 5px;' });
const chatBoxInner = createElement('div', { class: 'group-chat-box__chat-box-wrapper___DHYHJ' });
const chatBox = createElement('div', { class: 'chat-box___mHm01', style: 'width: 261.1px;' });
const chatBoxHeader = createElement('div', { role: 'button', tabindex: 0, class: 'chat-box-header___P15jw' });
addEvent(chatBoxHeader, 'click', (e) => {
if (e.target === chatBoxHeader) chatBoxWrapper.style.display = 'none';
});
const headerInfo = createElement('button', { type: 'button', class: 'chat-box-header__info___GZckD' });
const avatar = createElement('div', { class: 'chat-box-header__avatar___sctcZ' }, '🤖');
const name = createElement('p', { class: 'typography___Dc5WV body3 color-tornLightGrey chat-box-header__name___jIjjM' }, 'Cypher');
headerInfo.append(avatar, name);
const resetBtn = createElement('button', { type: 'button', class: 'chat-box-header__reset-btn', title: 'Reset Chat' }, '🔄');
addEvent(resetBtn, 'click', () => {
localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify([{ role: 'system', content: DEFAULT_SYSTEM_MESSAGE }]));
loadChatHistory();
});
chatBoxHeader.append(headerInfo, resetBtn);
const chatBoxBody = createElement('div', { class: 'chat-box-body___NWs3t', style: 'height: calc(251.54px - 30px);' });
if (!localStorage.getItem(CHAT_HISTORY_KEY) || JSON.parse(localStorage.getItem(CHAT_HISTORY_KEY)).length === 0) {
localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify([{ role: 'system', content: DEFAULT_SYSTEM_MESSAGE }]));
}
const dataSelect = createElement('select', { class: 'dropdown__button___e_Nvg', id: 'data-to-include' });
getDynamicOptions().forEach(optionText => dataSelect.add(createElement('option', { value: optionText }, optionText)));
const chatBoxFooter = createElement('div', { class: 'chat-box-footer___YK914' });
const textarea = createElement('textarea', { class: 'chat-box-footer__textarea___liho_', placeholder: 'Type your message here...', style: 'height: 15px;' });
const sendBtn = createElement('button', { type: 'button', class: 'chat-box-footer__send-icon-wrapper___fGx9E' }, ' প্রেরণ');
addEvent(sendBtn, 'click', () => {
sendMessage(textarea.value);
textarea.value = '';
});
chatBoxFooter.append(textarea, sendBtn);
chatBox.append(chatBoxHeader, dataSelect, chatBoxBody, chatBoxFooter);
chatBoxInner.appendChild(chatBox);
chatBoxWrapper.appendChild(chatBoxInner);
chatApp.querySelector('section[class^="chat-app__chat-list-chat-box-wrapper___"]').appendChild(chatBoxWrapper);
};
const addAssistantChatButton = () => {
const menuWrapper = $('section > div[class^="minimized-menus__root___"] div[class^="minimized-menus__wrapper___"]');
if (!menuWrapper) return;
const newButton = createElement('button', { type: 'button', title: ASSISTANT_BUTTON_TITLE, class: 'minimized-menu-item___w0wvp minimized-menu-item--open___w0wvp' }, '🤖');
addEvent(newButton, 'click', toggleAssistantChatBox);
menuWrapper.appendChild(newButton);
};
const addAssistantSettings = () => {
const settingsSection = $('div[class^="settings-panel___"] div[class^="settings-panel__section___"]');
if (!settingsSection) return;
const assistantSection = createElement('div', { class: 'settings-channel__item___jXaGx' });
const apiKeyContainer = createElement('div', { class: 'notification-item___ipQ1c' });
const apiKeyLabel = createElement('div', { class: 'notification-item__label___yWh4D' });
apiKeyLabel.innerHTML = '<p class="typography___Dc5WV body4 color-#333333">Huggingface API Key (Cypher)</p>';
const apiKeyInput = createElement('input', { type: 'password', placeholder: 'Enter API Key', id: 'assistant-api-key', style: 'width: 100%;', value: localStorage.getItem(API_KEY_KEY) || '' });
addEvent(apiKeyInput, 'input', () => localStorage.setItem(API_KEY_KEY, apiKeyInput.value));
apiKeyContainer.append(apiKeyLabel, apiKeyInput);
const enableToggleContainer = createElement('div', { class: 'notification-item___ipQ1c' });
const enableToggleLabel = createElement('div', { class: 'notification-item__label___yWh4D' });
enableToggleLabel.innerHTML = '<p class="typography___Dc5WV body4 color-#333333">Assistant Chat (Cypher)</p>';
const enableToggle = createElement('input', { type: 'checkbox', id: 'assistant-enable-toggle', checked: localStorage.getItem(ENABLED_KEY) === 'true' });
addEvent(enableToggle, 'change', () => {
localStorage.setItem(ENABLED_KEY, enableToggle.checked);
enableToggle.checked ? addAssistantChatButton() : $(`button[title="${ASSISTANT_BUTTON_TITLE}"]`)?.remove();
const assistantChatBox = $('.assistant-chat-box');
if (assistantChatBox) assistantChatBox.style.display = 'none';
});
enableToggleContainer.append(enableToggleLabel, enableToggle);
assistantSection.append(apiKeyContainer, enableToggleContainer);
const channelsHeader = $('p[class^="typography___Dc5WV"].settings-header___WQHmE', settingsSection);
const channelItems = channelsHeader?.nextElementSibling?.nextElementSibling;
channelItems ? channelItems.insertBefore(assistantSection, channelItems.firstChild) : settingsSection.appendChild(assistantSection);
};
// --- Data Functions ---
const getDynamicOptions = () => {
const options = ['Include nothing'];
Object.entries(dataDefinitions).forEach(([dataType, { selector }]) => {
if ($(selector)) options.push(dataType);
});
options.push('Current Page');
return options;
};
const fetchGameData = (dataType) => {
if (dataType === 'Current Page') {
return JSON.stringify({
title: document.title,
url: window.location.href,
content: document.body.innerText.slice(0, 500)
});
}
const dataDefinition = dataDefinitions[dataType];
return dataDefinition ? dataDefinition.dataExtractor($(dataDefinition.selector)) : '';
};
// --- Messaging Functions ---
const sendMessage = async (message) => {
const apiKey = localStorage.getItem(API_KEY_KEY);
const selectedData = $('#data-to-include').value;
let messages = JSON.parse(localStorage.getItem(CHAT_HISTORY_KEY) || '[]');
let serverTime = $('.server-date-time').innerText != "" ? $('.server-date-time').innerText : "Server time not found, if user asks for something time related, ask them to open the clock icon at the top of the page before sending a question so you can see the time and date.";
messages.push({ role: 'system', content: `Server Time: ${serverTime}` });
if (selectedData !== 'Include nothing') {
messages.push({ role: 'system', content: `Included data by user (${selectedData}): ${fetchGameData(selectedData)}` });
}
messages.push({ role: 'user', content: message });
if (!apiKey) {
console.error('No API key found in localStorage');
displayMessage('System', 'Please enter an API key in the settings.');
return;
}
displayMessage("user", message);
localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(messages));
try {
const response = await PDA_httpRequest('POST', API_URL, {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}, JSON.stringify({
"model": "Qwen/Qwen2.5-Coder-32B-Instruct",
"messages": messages,
"max_tokens": 500,
"stream": false
}));
console.log(JSON.parse(response.responseText));
if (response.error) throw new Error(response.error);
const assistantMessage = JSON.parse(response.responseText).choices[0].message.content;
displayMessage('Assistant', assistantMessage);
messages.push({ role: 'assistant', content: assistantMessage });
localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(messages));
} catch (error) {
console.error('Error:', error.message);
displayMessage('System', `Error: ${error.message}`);
}
};
const formatAssistantMessage = (message) => message
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/_(.*?)_/g, '<u>$1</u>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>');
const displayMessage = (sender, message) => {
const chatBoxBody = $('.assistant-chat-box .chat-box-body___NWs3t');
if (!chatBoxBody) return;
if (chatBoxBody.innerHTML === '<p>No messages yet. Start chatting!</p>') chatBoxBody.innerHTML = '';
const messageDiv = createElement('div', { class: 'chat-message' });
messageDiv.classList.add(sender === "Assistant" ? 'assistant-message' : 'user-message');
messageDiv.innerHTML = `<br><strong>${sender === "User" ? JSON.parse($('#torn-user').value).playername : sender}:</strong> ${sender === "Assistant" ? formatAssistantMessage(message) : message}`;
chatBoxBody.appendChild(messageDiv);
chatBoxBody.scrollTop = chatBoxBody.scrollHeight;
};
const loadChatHistory = () => {
const chatBoxBody = $('.assistant-chat-box .chat-box-body___NWs3t');
if (!chatBoxBody) return;
chatBoxBody.innerHTML = '';
const messages = JSON.parse(localStorage.getItem(CHAT_HISTORY_KEY) || '[]');
messages.filter(msg => msg.role !== 'system').forEach(msg => displayMessage(msg.role === 'user' ? 'User' : 'Assistant', msg.content));
};
// --- Event Handlers and Logic ---
const observer = new MutationObserver(() => {
if (!$('#assistant-api-key')) addAssistantSettings();
if (!$(`button[title="${ASSISTANT_BUTTON_TITLE}"]`) && $('#assistant-enable-toggle')?.checked) addAssistantChatButton();
const dataSelect = $('#data-to-include');
if (dataSelect) {
const currentOptions = Array.from(dataSelect.options).map(option => option.value);
const newOptions = getDynamicOptions();
if (JSON.stringify(currentOptions) !== JSON.stringify(newOptions)) {
dataSelect.innerHTML = '';
newOptions.forEach(optionText => dataSelect.add(createElement('option', { value: optionText }, optionText)));
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
if (!localStorage.getItem(CHAT_HISTORY_KEY)) localStorage.setItem(CHAT_HISTORY_KEY, '[]');
localStorage.getItem(ENABLED_KEY) === 'true' ? (addAssistantSettings(), addAssistantChatButton()) : addAssistantSettings();
})();