// ==UserScript==
// @name ChatGPT Enhance
// @name:en ChatGPT Enhance
// @name:zh-CN ChatGPT 增强
// @name:zh-TW ChatGPT 增強
// @name:ja ChatGPT 拡張
// @name:ko ChatGPT 향상
// @name:de ChatGPT verbessern
// @name:fr ChatGPT améliorer
// @name:es ChatGPT mejorar
// @name:pt ChatGPT melhorar
// @name:ru ChatGPT улучшить
// @name:it ChatGPT migliorare
// @name:tr ChatGPT geliştirmek
// @name:ar ChatGPT تحسين
// @name:th ChatGPT ปรับปรุง
// @name:vi ChatGPT cải thiện
// @name:id ChatGPT meningkatkan
// @namespace Violentmonkey Scripts
// @match *://chat.openai.com/*
// @match *://chatgpt.com/*
// @version XiaoYing_2024.08.08.2
// @grant GM_info
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getResourceText
// @grant GM_getResourceURL
// @grant GM_openInTab
// @grant unsafeWindow
// @run-at document-start
// @author github.com @XiaoYingYo
// @require https://greasyfork.org/scripts/464929-module-jquery-xiaoying/code/module_jquery_XiaoYing.js
// @require https://greasyfork.org/scripts/464780-global-module/code/global_module.js
// @require https://greasyfork.org/scripts/465643-ajaxhookerlatest/code/ajaxHookerLatest.js
// @require https://greasyfork.org/scripts/440334-jquery-like-spa-operation-library/code/jQuery-like%20SPA%20operation%20library.js
// @description 宽度对话框 & 一键清空聊天记录 & 向GPT声明指定语言回复
// @description:en Wide dialog & Clear chat history & Declare specified language reply to GPT
// @description:zh-CN 宽度对话框 & 一键清空聊天记录 & 向GPT声明指定语言回复
// @description:zh-TW 寬度對話框 & 一鍵清空聊天記錄 & 向GPT聲明指定語言回復
// @description:ja 幅広いダイアログ & チャット履歴をクリア & 指定された言語でGPTに宣言する
// @description:ko 넓은 대화 상자 & 채팅 기록 지우기 & 지정된 언어로 GPT에 선언
// @description:de Breites Dialogfeld & Chatverlauf löschen & GPT in angegebener Sprache deklarieren
// @description:fr Boîte de dialogue large & Effacer l'historique du chat & Déclarer la réponse dans la langue spécifiée à GPT
// @description:es Cuadro de diálogo ancho & Borrar el historial del chat & Declarar respuesta en el idioma especificado a GPT
// @description:pt Caixa de diálogo ampla & Limpar o histórico do bate-papo & Declarar resposta no idioma especificado ao GPT
// @description:ru Широкий диалоговое окно & Очистить историю чата & Объявить ответ на указанном языке в GPT
// @description:it Ampia finestra di dialogo & Cancella la cronologia della chat & Dichiarare la risposta nella lingua specificata a GPT
// @description:tr Geniş diyalog & Sohbet geçmişini temizle & GPT'ye belirtilen dilde yanıt bildir
// @description:ar مربع حوار واسع & مسح سجل المحادثة & إعلان الرد باللغة المحددة إلى GPT
// @description:th กล่องโต้ตอบกว้าง & ล้างประวัติการแชท & ประกาศการตอบกลับในภาษาที่ระบุไว้กับ GPT
// @description:vi Hộp thoại rộng & Xóa lịch sử trò chuyện & Khai báo trả lời bằng ngôn ngữ được chỉ định cho GPT
// @description:id Kotak dialog lebar & Hapus riwayat obrolan & Nyatakan balasan dalam bahasa yang ditentukan ke GPT
// @downloadURL none
// ==/UserScript==
// eslint-disable-next-line no-undef
ajaxHooker.protect();
var globalVariable = new Map();
var browserLanguage = navigator.language;
var ignoreHookStr = '&ignoreHookStr';
var clearButtonSvg = '';
(async function () {
function initSession() {
return new Promise((resolve) => {
$.get('/api/auth/session', { headers: { Accept: 'application/json', 'Content-Type': 'application/json' } }).done((res) => {
globalVariable.set('session', res);
globalVariable.set('accessToken', res.accessToken);
resolve();
});
});
}
function clearAllConversations() {
return new Promise(async (resolve) => {
let data = await getAllItems();
let Tasks = [];
for (let i = 0; i < data.length; i++) {
let item = data[i];
let id = item.id;
let title = item.title;
if (title.charAt(0) == '#') {
continue;
}
Tasks.push(deleteItem(id));
}
await Promise.all(Tasks);
resolve();
// $.ajax({
// type: 'PATCH',
// url: '/backend-api/conversations',
// headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${globalVariable.get('accessToken')}` },
// data: JSON.stringify({ is_visible: false }),
// success: (res) => {
// resolve(res);
// }
// });
});
}
function initClearButton() {
return new Promise(async (resolve) => {
let sureClearButtonSvg = '';
let oneBtn = await global_module.waitForElement('nav[aria-label]', null, null, 100, -1);
globalVariable.set('Nav', $(oneBtn));
oneBtn = oneBtn.find('button').eq(0);
let newBtn = global_module.cloneAndHide(oneBtn[0]);
newBtn = $(newBtn).eq(0).attr('status', 0);
oneBtn.show();
newBtn.find('svg').remove();
newBtn.append(clearButtonSvg);
newBtn.off('click').on('click', async () => {
let status = newBtn.attr('status');
newBtn.attr('disabled', 'disabled');
if (status == 0) {
newBtn.attr('status', 1);
newBtn.find('svg').remove();
newBtn.append(sureClearButtonSvg);
} else {
newBtn.attr('status', 0);
newBtn.find('svg').remove();
newBtn.append(clearButtonSvg);
await clearAllConversations();
unsafeWindow.location.href = '/';
}
newBtn.removeAttr('disabled');
});
globalVariable.set('oneBtn', oneBtn);
resolve();
});
}
function initIncognitoModeButton() {
return new Promise(async (resolve) => {
let oneBtn = globalVariable.get('oneBtn');
let yesSvg = '';
let noSvg = '';
oneBtn = oneBtn.eq(0);
let newBtn = global_module.cloneAndHide(oneBtn[0]);
newBtn = $(newBtn).eq(0).attr('status', 0);
oneBtn.show();
newBtn.find('svg').remove();
let incognitoMode = GM_getValue('incognitoMode', false);
let judgeToIncognitoMode = function (i) {
if (i) {
if (global_module.GetUrlParm(null, 'temporary-chat') != 'true') {
window.location.href = '/?temporary-chat=true';
}
} else {
if (global_module.GetUrlParm(null, 'temporary-chat') == 'true') {
window.location.href = '/';
}
}
};
if (!incognitoMode) {
newBtn.append(yesSvg);
} else {
newBtn.append(noSvg);
judgeToIncognitoMode(incognitoMode);
}
newBtn.off('click').on('click', async () => {
if (incognitoMode) {
newBtn.attr('status', 1);
newBtn.find('svg').remove();
newBtn.append(yesSvg);
} else {
newBtn.attr('status', 0);
newBtn.find('svg').remove();
newBtn.append(noSvg);
}
GM_setValue('incognitoMode', !incognitoMode);
incognitoMode = GM_getValue('incognitoMode', false);
judgeToIncognitoMode(incognitoMode);
});
resolve();
});
}
await initSession();
await initClearButton();
await initIncognitoModeButton();
})();
function purify() {
return new Promise(async (resolve) => {
let Tasks = [];
let nav = globalVariable.get('Nav');
Tasks.push(
(() => {
return new Promise(async (resolve) => {
let upgradeDom = await global_module.waitForElement('span[class*="border-token-border-light"]', null, null, 100, -1, nav);
upgradeDom = upgradeDom.eq(upgradeDom.length - 1);
upgradeDom.parents('a').eq(0).hide();
resolve();
});
})()
);
Tasks.push(
(() => {
return new Promise(async (resolve) => {
let presentation = await global_module.waitForElement('div[role="presentation"]', null, null, 100, -1);
presentation = presentation.eq(presentation.length - 1);
let presentationTip = await global_module.waitForElement('span:contains("ChatGPT ")', null, null, 100, -1, presentation);
presentationTip.hide();
resolve();
});
})()
);
Tasks.push(
(() => {
return new Promise(async (resolve) => {
$(await global_module.waitForElement('button[data-state="closed"][id^="radix"]:contains("?")', null, null, 100, -1)).hide();
resolve();
});
})()
);
await Promise.all(Tasks);
resolve();
});
}
function getAllItems() {
return new Promise(async (resolve) => {
let limit = 30;
let currentTotal = 0;
let retItems = [];
let getItems = function (offset) {
return new Promise(async (resolve) => {
$.ajax({
type: 'GET',
url: '/backend-api/conversations?offset=' + offset + '&limit=' + limit + '&order=updated',
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${globalVariable.get('accessToken')}` },
success: (data) => {
resolve(data);
},
error: async () => {
resolve(await getAllItems(offset));
}
});
});
};
let data = await getItems(0);
let total = data.total;
let items = data.items;
currentTotal = currentTotal + items.length;
retItems = retItems.concat(items);
while (currentTotal < total) {
data = await getItems(currentTotal);
total = data.total;
items = data.items;
currentTotal = currentTotal + items.length;
retItems = retItems.concat(items);
}
resolve(retItems);
});
}
function deleteItem(id) {
let item = globalVariable.get('itemDom')[id];
if (item && !globalVariable.get('deleteItem_' + id + '_loading_setInterval')) {
let textDom = item.find('[dir="auto"]').eq(0);
textDom.text('.');
globalVariable.set(
'deleteItem_' + id + '_loading_setInterval',
setInterval(() => {
textDom.text(textDom.text() + '.');
if (textDom.text().length > 6) {
textDom.text('.');
}
}, 500)
);
}
return new Promise(async (resolve) => {
$.ajax({
type: 'PATCH',
url: '/backend-api/conversation/' + id,
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${globalVariable.get('accessToken')}` },
data: JSON.stringify({ is_visible: false }),
success: () => {
if (item) {
item.remove();
}
resolve(true);
},
error: async () => {
resolve(await deleteItem(id));
}
});
});
}
function initItemDeleteBtn() {
return new Promise(async (resolve) => {
let nav = globalVariable.get('Nav');
let itemDiv = await global_module.waitForElement('div[class*="text-token-text-primary text-sm"]', null, null, 100, -1, nav);
let liList = await global_module.waitForElement('li', null, null, 100, -1, itemDiv);
globalVariable.set('itemDom', {});
for (let i = 0; i < liList.length; i++) {
let spanBtn = $(liList[i]).find('span[class][data-state="closed"]');
let that = spanBtn.eq(0);
let li = that.parents('li');
let a = li.find('a');
let href = a.attr('href');
let id = href.replace('/c/', '');
globalVariable.get('itemDom')[id] = li;
if (spanBtn.length != 1) {
continue;
}
let newBtn = global_module.cloneAndHide(that[0], 2);
newBtn = $(newBtn);
newBtn.find('svg').remove();
newBtn.append(clearButtonSvg);
that.show();
newBtn.css('cursor', 'pointer');
newBtn.off('click').on('click', async () => {
await deleteItem(id);
});
}
resolve();
});
}
function widescreenDialogue() {
return new Promise(async (resolve) => {
if ($('body').find('style[id="widescreenDialogueCss]').length != 0) {
resolve();
return;
}
let sel = 'textarea[id="prompt-textarea"]';
let Btn = await global_module.waitForElement(sel, null, null, 1000, -1);
Btn = Btn.eq(0);
let BtnParent = Btn.parents('form').eq(0).parent().eq(0);
let cssClassName = BtnParent.attr('class').split(' ')[0];
let styleHtml = '.' + cssClassName + '{width:100%;max-width:100%;}';
$('body').append('');
resolve();
});
}
var HookFun = new Map();
HookFun.set('/backend-api/conversation', function (req, res, Text, period) {
if (period === 'preload') {
let additional = 'Please reply me with ';
let additionals = additional + browserLanguage;
let body = JSON.parse(req.data);
let messages = body.messages;
if (messages instanceof Array) {
for (let i = 0; i < messages.length; i++) {
let parts = messages[i].content.parts;
if (parts instanceof Array) {
for (let j = 0; j < parts.length; j++) {
if (parts[j].indexOf(additional) != -1) {
continue;
}
parts[j] = parts[j] + '\n' + additionals;
}
}
}
}
req.data = JSON.stringify(body);
return;
}
return new Promise(async (resolve) => {
if (period !== 'done') {
resolve(null);
return;
}
setTimeout(async () => {
await widescreenDialogue();
}, 100);
resolve(null);
});
});
HookFun.set('/backend-api/conversations', function (req, res, Text, period) {
return new Promise(async (resolve) => {
if (period !== 'done') {
resolve(null);
return;
}
setTimeout(async () => {
await initItemDeleteBtn();
}, 1000);
resolve(null);
});
});
function handleResponse(request) {
if (!request) {
return;
}
if (request.url.indexOf(ignoreHookStr) != -1) {
return;
}
let tempUrl = request.url;
if (tempUrl.indexOf('http') == -1 && tempUrl[0] == '/') {
tempUrl = location.origin + tempUrl;
}
let pathname = new URL(tempUrl).pathname;
let fun = HookFun.get(pathname);
if (!fun) {
return;
}
fun(request, null, null, 'preload');
request.response = (res) => {
let Type = 0;
let responseText = res.responseText;
if (typeof responseText !== 'string') {
Type = 1;
responseText = res.text;
}
if (typeof responseText !== 'string') {
Type = 2;
responseText = JSON.stringify(res.json);
}
const oldText = responseText;
res.responseText = new Promise(async (resolve) => {
let ret = await fun(request, res, responseText, 'done');
if (!ret) {
ret = oldText;
}
if (Type === 2) {
if (typeof ret === 'string') {
ret = JSON.parse(ret);
}
}
resolve(ret);
});
};
}
// eslint-disable-next-line no-undef
ajaxHooker.hook(handleResponse);
$.onurlchange(function () {
setTimeout(async () => {
await widescreenDialogue();
await purify();
}, 1000);
});