南瓜博士:用GPT写的油猴脚本
作者:南瓜博士
项目开源地址:https://github.com/1766left/EasyFill
🔧EasyFill 是一个油猴脚本工具,可以在 GPT上使用。它可以帮助用户快速填写选中的文字,并发送到聊天窗口。用户可以选择不同的功能组,并自定义功能的模版。这个工具的便捷之处在于可以方便地选取前面讨论的部分内容,并添加详细指导的模版,省去了很多来回复制粘贴的工作。
代码在不断迭代中。推荐到 https://github.com/1766left/EasyFill 看最新版哈。
EasyFill
添加油猴脚本后在 chat.openai.com 中使用。
最直接的功能——通过菜单选择模版发送 Prompt:
更有用的功能——将选中的内容填充进模版后再发送。
点选菜单文字按钮是直接发送,如果点右侧的图标则是填充到聊天文本框里,编辑后再发送。
菜单上每一项对应一个 Prompt 模版,Prompt 可以根据使用场景放在不同的功能组里,通过“设置”来切换或编辑功能组。
另外有个我超级喜欢的功能,是把用得不错的 Prompt 添加到模版里。我之前在 一个一串一串串——比提问更重要的是追问 里有提到,其实我平时和 GPT 聊天比较随意,很自由地按需调整对话内容。这样一来时不时会遇到些特别有效的对话,选中了内容再点击“添加为模版”,把其中部分修改成 PLACE_HOLDER 的占位符,后续就能反复使用了。
如果你让 GPT 按指定格式输出内容,脚本还可以把文字变成直接可点击的项,连选中文字的麻烦都省了。 这是我写的一个功能组示范:
启用该功能组之前 GPT 的原始输出是这样的:
启用后这些特殊 emoji 框起来的内容就都成了可以点击的链接,点后自动补充上“查询英文和当地语言的维基百科并用中文回答我”之类的要求发送出去,真的超方便。
有了这样的功能后,哪怕复杂的游戏,都能顺畅进行了。有图为证——
工具分享给朋友们用后,收到了很多反馈。以下功能要感谢朋友们的建议:
- 菜单可以固定在屏幕右侧。固定菜单的好处,一是对于不需要选择文字的 prompt 用起来会更方便;二是对于日常用 GPT 进行的操作,一般说来可以总结成多个步骤,有这么一个菜单在旁边,也可以当作流程提醒。
- 我们不只是在最后的输入框输 prompt,有时候会回到之前的对话重新问。因此,只要中间有编辑框是开启的,当点击菜单右侧小按钮时,就会把内容在所有编辑框里都填充上。
- 有小伙伴不希望菜单在不需要的时候弹出,因此增加了 Shift+左键的模式(在设置中可以和快捷模式切换),开启后要选择文字同时要按住 Shift 键才会有菜单。
差不多就这些了。也可以看视频获取直观感受
脚本已开源。感兴趣的小伙伴们,点击阅读原文去 https://github.com/1766left/EasyFill 自取。用把跟目录下的 js 贴到油猴脚本里就能用,或者 chrome 插件的 zip 文件下载了用也行。觉得好的请给 repo 点个 Star,我能有动力继续叠加新功能。
其实这些功能挺简单的,但 GPT 用得多的小伙伴们一定能感受到它多实用。特别欢迎使用插件的伙伴们和我分享你是怎么用的。工具只是便捷操作的一小步,组合了 prompt 设计好的工作流,才是发挥 AI 功力的最重要因素。所以欢迎关注南瓜博士微信公众号后台回复 easyfill, 获取入群二维码,在群里交流使用心得和提新需求。
使用方法
选中页面上任意一段文字,点击按钮,工具就会把你选中的内容填入 {__PLACE_HOLDER__} 的位置并发送。 如果想要编辑下再发送,可以点击右侧的铅笔图标。 最后一个“设置”可用于修改菜单项。
这个工具的便捷之处,在于你可以方便地选取前面讨论的部分内容,并添加详细指导的 prompt,省去了很多来回 copy&paste 的工作。 可以参考这个使用示范 理解工具的用途。请注意那些看着就很麻烦的 prompt 都是自动填充模版生成的。
JS代码
代码最近在不断迭代中。推荐到 https://github.com/1766left/EasyFill 看最新版哈。
- 浏览器中安装Tampermonkey:https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo
- 将以下内容黏贴到 Tampermonkey中:
// ==UserScript==
// @name EasyFill
// @namespace http://easyfill.tool.elfe/
// @version 0.9
// @description 超级方便的 GPT 对话助手,通过划选或点击,把内容填充到预置 prompt 模版直接发送。支持多个功能组设置。
// @author Elfe & ttmouse & GPT
// @match https://chat.openai.com/*
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAABX2lDQ1BJQ0MgUHJvZmlsZQAAeJxtkLFLAmEYxh/NEI6LFCIaghyiycLOgla1qMDh0ARrO8/rDPT8uLuQtoJaQ6ihtrClsakWh9amgqAhotr6AyIXk+v9vEqtvo+X58fD+768PIBXVBgr+gCUDNtMLcZD2dW1kP8VArwYQBCColosJstJasG39r7GPTxc7yb5ruB+9rop1dM7lef4WOD86G9/zxPymqWSflBJKjNtwBMhlis247xNPGTSUcSHnHWXzzjnXK63e1ZSCeJb4oBaUPLEL8ThXJevd3GpuKl+3cCvFzUjkyYdphrFPBaQpB9CBhJmMU21RBn9PzPTnkmgDIYtmNiAjgJsmo6Rw1CERrwMAyqmECaWEKGK8qx/Z9jxyjVg7h3oq3a83DFwuQeMPHS88RNgcBe4uGGKqfwk62n4rPWo5LIYB/qfHOdtAvAfAK2q4zRrjtM6pf2PwJXxCXhkY9XHGXyzAAAAVmVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAADkoYABwAAABIAAABEoAIABAAAAAEAAAEgoAMABAAAAAEAAAEgAAAAAEFTQ0lJAAAAU2NyZWVuc2hvdPDFp1oAAABUSURBVHic3VFBDgAgCMLW/79M1wiX1TFuAhOcwK/gPLTKnRhYGFRH38u2gQAQMxOmK6MjTU4LUHpkJWWLlAw/oi65RhQlPeHpWduMO/sZ1l84+QUG1/URFizO5xoAAAAASUVORK5CYII=
// @grant none
// ==/UserScript==
// Copyright (c) 2023 ElfeXu (Xu Yanfei)
// Licensed under the MIT License.
const setting_usage_text = `使用说明
通过 🪄 分隔按钮
📖 之后的是直接在原文替代成链接的内容
🪄🪄🪄🪄🪄🪄🪄🪄
功能一
这里是预设的 prompt ,{__PLACE_HOLDER__} 里的内容会被你鼠标选中的文字替代掉。
🪄🪄🪄🪄🪄🪄🪄🪄
功能二
点击菜单文字可以直接发送,点击右边会把 prompt 填充到输入框,可以编辑后再发送。
🪄🪄🪄🪄🪄🪄🪄🪄
CLICK 示范
通过要求 GPT 以特定格式生成内容,可以将内容转化成链接,点击即直接发送。例如
请给我五个和有水关的英文单词,用两个方括号 [[]] 来标记。
再用列表的方式给出三个和水有关的节日,节日名称写在 💦 💦 之间
📖📖📖📖📖📖📖📖
\[\[(.*?)\]\]
请帮我解释一下{__PLACE_HOLDER__}这个词的意思
📖📖📖📖📖📖📖📖
💦(.*?)💦
请帮我详细介绍一下{__PLACE_HOLDER__}。
`
const setting_new_setting_text = `新功能组名称
这里可以填写功能组使用说明
通过 🪄 分隔按钮
🪄🪄🪄🪄🪄🪄🪄🪄
第一行是按钮名称
第二行开始是prompt。{__PLACE_HOLDER__} 里的内容会被你鼠标选中的文字替代掉。
🪄🪄🪄🪄🪄🪄🪄🪄
第二个功能
第二个prompt
prompt多长都没关系
各种奇怪字符也都可以用
只根据连续八个🪄来分隔功能
📖📖📖📖📖📖📖📖
\[\[(.*?)\]\]
在按钮之后可以用八个📖分隔,带上点击直接发送的内容。
第一行是正则匹配,后面是模版。匹配到的内容会替代掉{__PLACE_HOLDER__}中的内容然后被直接发送。
`;
const default_setting_texts = [
`英语练习
先点启动,再贴大段文章,然后需要干啥就选中了文字点啥功能
🪄🪄🪄🪄🪄🪄🪄🪄
启动
你是我的英语老师,我需要你陪我练习英语,准备托福考试。
请**用英语和我对话**,涉及英语例句、题目和话题探讨时请用托福水平的书面英语,但在我明确提出需要时切换到中文。
为了让我的学习更愉悦,请用轻松的语气,并添加一些 emoji。
接下来我会给你一篇英文文章,请记住文章,然后我会向你请求帮助。
如果你理解了,请说 Let's begin!
🪄🪄🪄🪄🪄🪄🪄🪄
英译中
请帮我把下面这段话翻译直译成中文,不要遗漏任何信息。
然后请判断文字是否符合中文表达习惯,如果不太符合,请重新意译,在遵循愿意的前提下让内容更通俗易懂。
输出格式应该是
直译:直译的内容
---
(如果有必要的话)意译:意译的内容
待翻译的内容:
'''
{__PLACE_HOLDER__}
'''
🪄🪄🪄🪄🪄🪄🪄🪄
中译英
请帮我用最地道的方式帮我把下面这段话翻译成英文。
待翻译的内容:
'''
{__PLACE_HOLDER__}
'''
🪄🪄🪄🪄🪄🪄🪄🪄
学单词
'''
{__PLACE_HOLDER__}
'''
请帮我学习这个单词
1. 请给出单词的音标、词性、中文意思、英文意思
2. 如果我们前面的讨论中出现过这个单词,请结合它的上下文,重点讲解在上下文中单词的意思和用法
3. 请给出更多例句
4. 如果有容易混淆的单词,请给出对比
🪄🪄🪄🪄🪄🪄🪄🪄
深入解释
我不太理解这段文字的具体含义,能否结合上下文,给我一个更深入的中文解释?
解释时请着重讲解其中有难度的字词句。
如果有可能,请为我提供背景知识以及你的观点。
'''
{__PLACE_HOLDER__}
'''
🪄🪄🪄🪄🪄🪄🪄🪄
封闭题
请对下面这段文字,按照托福阅读理解的难度,用英文为我出三道有标准答案的问答题。
请等待我回答后,再告诉我标准答案,并加以解释。
'''
{__PLACE_HOLDER__}
'''
🪄🪄🪄🪄🪄🪄🪄🪄
开放题
请对下面这段文字,按照托福口语和作文的难度,用英文为我出一道开放题,我们来进行探讨。
'''
{__PLACE_HOLDER__}
'''
`,
setting_usage_text
];
const LSID_SETTING_TEXTS = 'setting_texts_v0.4';
const LSID_SETTING_CURRENT_INDEX = 'setting_current_index_v0.4';
const LSID_MENU_MODE = 'setting_menu_mode'
////////////////////////// CSS //////////////////////////
const style = `
.settings-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.settings-content {
background-color: #f0f1ee;
color: #535e5e;
padding: 20px;
width: 50%;
height: 80%;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 10px;
display: flex;
flex-direction: column;
padding: 20px;
gap: 10px;
}
.buttonsContainer {
display: flex;
justify-content: space-between;
align-items: center; /* 确保子元素在垂直方向上居中 */
width: 100%;
}
.settings-dropdown {
outline: none;
border: 0px;
}
.settings-input {
width: 100%;
padding: 8px 20px;
background-color: #fff;
color: #000;
border: 0;
border-radius: 5px;
}
.settings-textarea {
width: 100%;
height: calc(100% - 60px);
resize: vertical;
background-color: #fff;
color: #000;
border-radius: 0.75em;
border: 0px;
padding: 18px 18px;
box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 0.5px, rgba(0, 0, 0, 0.024) 0px 0px 5px, rgba(0, 0, 0, 0.05) 0px 1px 2px;
}
.settings-button {
background-color: #469c7b;
color: #fff;
padding: 8px 18px;
border: none;
border-radius: 30px;
cursor: pointer;
margin: 0 5px;
}
.settings-button:hover {
background-color: #93B1A6;
}
.settings-button:disabled {
background-color: #B4B4B3; /* 灰色背景 */
color: #808080; /* 深灰色文字 */
cursor: not-allowed; /* 禁用的光标样式 */
}
.setting-confirm {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #f0f1ee;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: none;
flex-direction: column;
gap: 10px;
padding: 20px;
z-index: 2000;
}
.confirm-content {
color: #535e5e;
display: flex;
flex-direction: column;
gap: 10px;
}
#contextMenu {
display: none;
position: absolute;
}
.pinned-menu {
position: fixed;
right: 10px;
top: 50%;
transform: translateY(-50%); /* 这将使元素垂直居中,无论其高度是多少 */
}
#menuContainer {
width: auto;
display: inline-block;
background-color: #fff;
color: #000;
border-radius: 0.55em;
padding: 5px;
box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 0px 0.5px, rgba(0, 0, 0, 0.1) 0px 2px 5px, rgba(0, 0, 0, 0.05) 0px 3px 3px;
border-bottom: 1px solid #f0f0f0;
}
#menuContainer div {
display: flex;
align-items: center;
width: auto;
padding: 2px 0;
margin: 0px;
}
#menuContainer div.menu-title {
display: flex;
align-items: center;
justify-content: center;
padding: 5px 0;
font-weight: bold;
}
#menuContainer div.menu-separator {
width: 100%;
height: 1px;
background-color: #f0f0f0;
margin: 2px 0;
padding: 0px 10px;
}
#menuContainer div.menu-item {
display: flex;
align-items: center;
width: auto;
max-width: 200px;
min-width: 120px;
padding: 0 0;
margin: 0 5px;
}
#menuContainer button.menu-button {
border: none;
background: none;
padding: 5px 10px;
margin: 0;
white-space: nowrap;
border-radius: 5px;
transition: background-color 0.3s ease;
}
#menuContainer button.menu-button:hover {
background-color: #469c7b5c;
}
#menuContainer button.menu-button:disabled {
height: 1px;
color: #c6c6c600;
padding: 0;
border-bottom: 1px solid #dddddd8c;
}
#menuContainer button.menu-button:disabled:hover {
background: none;
}
#menuContainer button.left-part {
flex-grow: 1;
flex-shrink: 0;
flex-basis: auto;
max-width: 160px;
text-align: left;
overflow: hidden; /* 这会确保内容被裁剪 */
white-space: nowrap; /* 防止文本换行 */
text-overflow: ellipsis;/* 超出的文本将显示为... */
}
#menuContainer button.right-part {
flex-grow: 0;
flex-shrink: 0;
width: 40px;
text-align: right;
}
#menuContainer button.icon {
width: 24px;
height: 24px;
}
/* 使链接看起来更像链接 */
.custom-link {
text-decoration: underline;
cursor: pointer; /* 当鼠标放上去时变成手的样子 */
position: relative; /* 为了定位 tooltip */
}
/* 提示(tooltip)样式 */
.custom-link:hover::after {
content: attr(data-text); /* 显示 data-text 的内容 */
position: absolute;
bottom: 100%; /* 出现在链接的上方 */
left: 0; /* 与链接的左边界对齐 */
width: max-content; /* 根据内容设置宽度 */
max-width: 300px; /* 设置最大宽度 */
white-space: normal; /* 允许文本换行 */
line-height: 18px;
max-height: 120px;
overflow: hidden;
background-color: #333; /* 背景色 */
color: white; /* 文字颜色 */
padding: 5px 10px; /* 内边距 */
border-radius: 4px; /* 边框圆角 */
font-size: 12px; /* 文字大小 */
z-index: 10; /* 保证 tooltip 出现在其他元素的上方 */
}
/* 添加一个小三角形在 tooltip 的下方 */
.custom-link:hover::before {
content: '';
position: absolute;
bottom: -5px; /* 出现在 tooltip 的正下方 */
left: calc(50% + 5px); /* 让小三角形出现在链接的中央 */
width: 10px; /* 宽度 */
height: 10px; /* 高度 */
background-color: #333; /* 与 tooltip 的背景色相同 */
z-index: 9; /* 出现在 tooltip 下方 */
}
`;
const svgEditBeforeSend = "M896 128.426667H128c-47.146667 0-85.333333 38.186667-85.333333 85.333333V384h85.333333V212.906667h768v598.613333H128V640H42.666667v171.093333c0 47.146667 38.186667 84.48 85.333333 84.48h768c47.146667 0 85.333333-37.546667 85.333333-84.48v-597.333333c0-47.146667-38.186667-85.333333-85.333333-85.333333zM469.333333 682.666667l170.666667-170.666667-170.666667-170.666667v128H42.666667v85.333334h426.666666v128z";
const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);
////////////////////////// Send Prompt functions //////////////////////////
let setting_texts = JSON.parse(localStorage.getItem(LSID_SETTING_TEXTS)) || default_setting_texts;
let setting_current_index = localStorage.getItem(LSID_SETTING_CURRENT_INDEX) || 0;
let current_setting_text = setting_texts[setting_current_index];
let menu_mode = localStorage.getItem(LSID_MENU_MODE) || ''; // 后续还可以支持更多自定义模式。目前 '' 代表默认模式,即点击鼠标直接出菜单;非 '' ('shift')代表需要同时按住 shift 键才会出菜单
function replace_all_textarea(text) {
// 查找所有匹配按钮文本 "Save & Submit" 的 div
let buttons = Array.from(document.querySelectorAll('div.flex.w-full.gap-2.items-center.justify-center'));
buttons.forEach(button => {
// 检查按钮文本是否为 "Save & Submit"
if (button.textContent.trim() === 'Save & Submit') {
// 向上查找其祖先元素,直到找到一个拥有 `flex flex-grow flex-col gap-3 max-w-full` 这个 class 的 div
let parentDiv = button.closest('.flex.flex-grow.flex-col.gap-3.max-w-full');
if (parentDiv) {
// 在这个祖先 div 内,查找 `textarea` 元素
let textarea = parentDiv.querySelector('textarea');
if (textarea) {
// 替换这个 `textarea` 的内容为 "TEMPLATE_TEXT"
textarea.value = text;
}
}
}
});
}
async function sendToGPT(template, selectedText, sendDirectly) {
let placeholderPosition = template.indexOf('{__PLACE_HOLDER__}');
let finalText = template.replace('{__PLACE_HOLDER__}', selectedText);
if (!sendDirectly) {
replace_all_textarea(finalText);
}
const inputElement = document.getElementById('prompt-textarea');
inputElement.value = finalText;
const inputEvent = new Event('input', { 'bubbles': true });
inputElement.dispatchEvent(inputEvent);
await new Promise(resolve => setTimeout(resolve, 50));
if (sendDirectly) {
const sendButton = document.querySelector('[data-testid="send-button"]');
if (sendButton) {
sendButton.click();
}
inputElement.focus();
} else {
inputElement.focus();
// 设置光标位置
let cursorPosition;
if (placeholderPosition !== -1) {
// 将光标放在替换文本的结束位置
if (selectedText) {
cursorPosition = placeholderPosition + selectedText.length;
} else {
cursorPosition = placeholderPosition;
}
} else {
cursorPosition = inputElement.value.length; // 光标放在文本末尾
}
inputElement.setSelectionRange(cursorPosition, cursorPosition);
}
}
////////////////////////// Context Menu functions //////////////////////////
// 创建上下文菜单
const contextMenu = document.createElement('div');
const menuContainer = document.createElement('div');
let isMenuPinned = false;
function menuMode() {
return menu_mode;
}
function menuModeText() {
if (menuMode() == '') {
return '快捷模式'
} else {
return 'Shift模式'
}
}
function switchMode() {
if (menuMode() == '') {
menu_mode = 'shift';
} else {
menu_mode = '';
}
localStorage.setItem(LSID_MENU_MODE, menu_mode);
}
function createPathElement(svgPathData) {
// 创建一个`path`元素并设置SVG路径数据
const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathElement.setAttribute("d", svgPathData);
pathElement.setAttribute("fill", "#5D5D5D");
return pathElement;
}
function createPinButton() {
const pinButton = document.createElement('button');
pinButton.innerHTML = '📌'; // 使用pin emoji作为按钮的内容
pinButton.style.position = 'absolute';
pinButton.style.right = '5px';
pinButton.style.top = '5px';
pinButton.onclick = function() {
isMenuPinned = !isMenuPinned;
pinButton.innerHTML = isMenuPinned ? '🔓' : '📌';
if (isMenuPinned) {
menuContainer.classList.add('pinned-menu');
} else {
menuContainer.classList.remove('pinned-menu');
}
};
return pinButton;
}
function createMenuTitle() {
const menuTitle = document.createElement('div');
menuTitle.classList.add('menu-title');
menuTitle.innerHTML = setting_current_index;
menuTitle.innerHTML = setting_texts[setting_current_index].split('\n')[0];
return menuTitle;
}
function createMenuSeparator() {
const separator = document.createElement('div');
separator.classList.add('menu-separator');
return separator;
}
// 创建单个菜单项
function createMenuItem(label, icon, action1, action2) {
const menuItem = document.createElement('div');
menuItem.classList.add('menu-item');
const leftPart = document.createElement('button');
leftPart.classList.add('menu-button', 'left-part');
leftPart.innerHTML = label;
if (action1 == null) {
leftPart.disabled = true;
} else {
leftPart.onclick = () => {
action1();
hideContextMenu();
};
}
menuItem.appendChild(leftPart);
if (action2 != null) {
const rightPart = document.createElement('button')
rightPart.classList.add('menu-button', 'right-part');
if (icon != null) {
// 创建一个SVG元素并设置属性
const rightIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
rightIcon.setAttribute("viewBox", "0 0 1024 1024"); // 这个属性需要保留在这里
rightIcon.classList.add("icon");
rightIcon.appendChild(createPathElement(icon));
rightPart.appendChild(rightIcon);
} else {
rightPart.innerHTML = '≋';
}
rightPart.onclick = () => {
action2();
hideContextMenu();
};
menuItem.appendChild(rightPart);
}
return menuItem;
}
function hideContextMenu() {
if (isMenuPinned) {
return;
}
contextMenu.style.display = 'none';
}
function showContextMenu(event) {
const margin = 20;
const width = contextMenu.offsetWidth + margin;
const height = contextMenu.offsetHeight + margin;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 如果菜单超出了右侧窗口边缘
if (event.clientX + width > windowWidth) {
contextMenu.style.left = `${windowWidth - width}px`;
} else {
contextMenu.style.left = `${event.clientX}px`;
}
// 如果菜单超出了底部窗口边缘
if (event.clientY + height > windowHeight) {
contextMenu.style.top = `${windowHeight - height}px`;
} else {
contextMenu.style.top = `${event.clientY}px`;
}
contextMenu.style.display = 'block';
}
function updateMenuItems() {
parseSettingsText(current_setting_text);
menuContainer.innerHTML = '';
menuContainer.appendChild(createPinButton());
menuContainer.appendChild(createMenuTitle());
menuContainer.appendChild(createMenuSeparator());
menus.forEach((menu, index) => {
menuContainer.appendChild(
createMenuItem(
menu[0],
svgEditBeforeSend,
async function() {
await sendToGPT(menu[1], window.getSelection().toString().trim(), true);
},
async function() {
await sendToGPT(menu[1], window.getSelection().toString().trim(), false);
},
));
});
menuContainer.appendChild(createMenuSeparator());
menuContainer.appendChild(createMenuItem('设置', null, function() {showSettingsModal();}, null));
menuContainer.appendChild(createMenuItem('添加为模版', null, function() {showAddTemplateModal(window.getSelection().toString().trim());}, null));
}
function isMenuVisible() {
return contextMenu.style.display == 'block';
}
function shouldResponseForContextMenu(event) {
if (isMenuPinned) {
if (!isMenuVisible()) {
// Should not be here. Just to make sure pin is removed if menu is not visible.
isMenuPinned = false;
}
return false;
}
// 查找 settings-modal,如果 settings-modal 存在,就不响应右键菜单
const settingsModal = document.querySelector('.settings-modal');
if (settingsModal) {
return false;
}
if (menuMode() != '' && !event.shiftKey) {
return false;
}
return true;
}
function initContextMenu() {
contextMenu.id = 'contextMenu';
menuContainer.id = 'menuContainer';
contextMenu.appendChild(menuContainer);
document.body.appendChild(contextMenu);
updateMenuItems();
document.addEventListener('mouseup', function(event) {
if (!shouldResponseForContextMenu(event)) {
return;
}
const selectedText = window.getSelection().toString();
if (selectedText.length == 0) {
hideContextMenu();
} else {
showContextMenu(event);
}
});
document.addEventListener('dblclick', function(event) {
if (!shouldResponseForContextMenu(event)) {
return;
}
showContextMenu(event);
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
hideContextMenu();
}
});
}
////////////////////////// Easy Click functions //////////////////////////
let isUpdating = false;
let rerunTimeout;
let intervalID;
let shouldContinue = true;
// 点击事件处理器
async function clickHandler(event) {
//event.preventDefault();
console.log('执行 clickHandler'); // 执行点击事件处理器
const inputElement = document.getElementById('prompt-textarea'); // 获取输入框
console.log('[Debug] 获取输入框元素');
inputElement.value = this.getAttribute("data-text"); // 将链接文本添加到输入框
console.log('[Debug] 设置输入框值');
const inputEvent = new Event('input', { 'bubbles': true }); // 创建input事件
console.log('[Debug] 创建 input 事件');
inputElement.dispatchEvent(inputEvent); // 触发input事件
console.log('[Debug] 触发 input 事件');
await new Promise(resolve => setTimeout(resolve, 50));
console.log('[Debug] 50ms 延时完成');
const sendButton = document.querySelector('[data-testid="send-button"]');
console.log('[Debug] 获取发送按钮');
if (sendButton) {
console.log('点击发送按钮');
sendButton.click();
console.log('[Debug] 开启监听');
}
}
function replace_text(original) {
clicks.forEach(([regExpression, template]) => {
original = original.replace(regExpression, (match, p1) => {
// 使用模板替换找到的匹配项
let replaced = template.replace(/{__PLACE_HOLDER__}/g, p1);
if (template.includes('{__PLACE_HOLDER__}')) {
return `<a class="custom-link" data-text="${replaced}">${p1}</a>`;
} else {
return `<a class="custom-link" data-text="${replaced}">${regExpression.source}</a>`;
}
});
});
return original;
}
// 处理元素
function processElement(element) {
if (isUpdating) {
return;
}
const hidden_characters = "\u200B\u200B\u200B\u200B\u200B\u200B";
let innerHTML = element.innerHTML;
if (innerHTML.startsWith(hidden_characters)) {
return; // 不重复处理
}
// 处理[[ ]] 符号
const bracketRegex = /\[\[(.*?)\]\]/g;
innerHTML = replace_text(innerHTML);
// 替换了 innerHTML 后,原本网页中 Copy code 之类的事件监听就失效了。
// 为了尽可能让两个功能共存,这里仅对文字有改动的重新赋值。
if (innerHTML != element.innerHTML) {
element.innerHTML = hidden_characters + innerHTML; // 添加隐藏字符,避免重复处理
}
// 给新生成的链接添加事件监听
const customLinks = element.querySelectorAll('.custom-link');
customLinks.forEach(link => {
link.addEventListener('click', clickHandler);
});
}
function processAllElements() {
// 先找到父级对象
const parentElements = document.querySelectorAll('.flex.flex-grow.flex-col.gap-3.max-w-full');
parentElements.forEach(parent => {
// 在父级对象下面找特定的子元素
const chatRecordElements = parent.querySelectorAll('div.markdown.prose.w-full.break-words,li');
chatRecordElements.forEach(processElement);
});
}
// 这个部分是用来检测GPT是否在更新的
function checkUpdateStatus() {
if (!shouldContinue) return;
console.log('[Debug] 运行 checkUpdateStatus');
const allButtons = document.querySelectorAll('button');
const stopGeneratingElement = Array.from(allButtons).find(el => el.textContent.includes("Stop generating"));
if (!stopGeneratingElement && isUpdating) { // 内容更新完成
console.log('内容更新完成,准备添加链接');
isUpdating = false; // 更新状态设置为false
if (rerunTimeout) {
clearTimeout(rerunTimeout); // 清除延时
console.log('[Debug] 清除之前的延时');
}
// 设置延时,等待2秒
rerunTimeout = setTimeout(() => {
processAllElements();
stopListening();
}, 300);
}
}
function startListening() {
isUpdating = true;
shouldContinue = true;
intervalID = setInterval(checkUpdateStatus, 1000);
console.log('监听已开启');
}
function stopListening() {
shouldContinue = false;
clearInterval(intervalID);
console.log('监听已停止');
}
////////////////////////// Settings functions //////////////////////////
function closeModal(settingConfirm) {
settingConfirm.style.display = 'flex';
}
function createConfirmModal(modal) {
const settingConfirm = document.createElement('div');
settingConfirm.className = 'setting-confirm';
const confirmContent = document.createElement('div');
confirmContent.className = 'confirm-content';
const confirmText = document.createElement('p');
confirmText.textContent = '确定放弃修改?';
const confirmYes = document.createElement('button');
confirmYes.className = 'settings-button';
confirmYes.textContent = '确定';
const confirmNo = document.createElement('button');
confirmNo.className = 'settings-button';
confirmNo.textContent = '取消';
confirmYes.onclick = function() {
modal.remove();
settingConfirm.style.display = 'none'; // Hide the confirm box
}
confirmNo.onclick = function() {
settingConfirm.style.display = 'none'; // Hide the confirm box
}
confirmContent.appendChild(confirmText);
confirmContent.appendChild(confirmYes);
confirmContent.appendChild(confirmNo);
settingConfirm.appendChild(confirmContent);
return settingConfirm;
}
function showSettingsModal() {
const modal = document.createElement('div');
modal.className = 'settings-modal';
const modalContent = document.createElement('div');
modalContent.className = 'settings-content';
const textarea = document.createElement('textarea');
textarea.className = 'settings-textarea';
textarea.value = current_setting_text;
const submitButton = document.createElement('button');
submitButton.className = 'settings-button';
submitButton.textContent = '保存设置';
const cancelButton = document.createElement('button');
cancelButton.className = 'settings-button';
cancelButton.textContent = '取消修改';
const settingsDropdown = document.createElement('select');
settingsDropdown.className = 'settings-dropdown';
setting_texts.forEach((text, index) => {
const option = document.createElement('option');
option.value = index;
option.text = text.split('\n')[0]; // Assuming the first line is a title or identifier
settingsDropdown.appendChild(option);
});
settingsDropdown.selectedIndex = setting_current_index;
settingsDropdown.addEventListener('change', (e) => {
const selectedIndex = e.target.value;
textarea.value = setting_texts[selectedIndex];
if (setting_texts.length <= 1) {
deleteSettingButton.disabled = true;
} else {
deleteSettingButton.disabled = false;
}
});
const newSettingButton = document.createElement('button');
newSettingButton.textContent = '添加新功能组';
newSettingButton.className = 'settings-button';
newSettingButton.addEventListener('click', () => {
textarea.value = setting_new_setting_text;
setting_texts.push(textarea.value);
const option = document.createElement('option');
option.value = setting_texts.length - 1;
option.text = setting_new_setting_text.split('\n')[0];
settingsDropdown.appendChild(option);
settingsDropdown.value = setting_texts.length - 1;
deleteSettingButton.disabled = false;
});
const deleteSettingButton = document.createElement('button');
deleteSettingButton.textContent = '删除当前功能组';
deleteSettingButton.className = 'settings-button';
deleteSettingButton.addEventListener('click', () => {
// 如果只剩一个设置,则不进行删除操作
if (setting_texts.length <= 1) {
return;
}
let toDelete = settingsDropdown.selectedIndex;
// 从 setting_texts 数组中删除设置
setting_texts.splice(toDelete, 1);
// 从 settingsDropdown 中删除对应的选项
settingsDropdown.remove(toDelete);
// 如果删除的是第0项或列表中的最后一项,则默认选择第0项
if (toDelete === 0 || toDelete === setting_texts.length) {
settingsDropdown.selectedIndex = 0;
setting_current_index = 0;
} else {
// 否则选择之前的项
settingsDropdown.selectedIndex = toDelete - 1;
setting_current_index = toDelete - 1;
}
// 更新文本区的值为当前选中的设置
textarea.value = setting_texts[setting_current_index];
// 保存到 localStorage
localStorage.setItem(LSID_SETTING_TEXTS, JSON.stringify(setting_texts));
localStorage.setItem(LSID_SETTING_CURRENT_INDEX, setting_current_index);
deleteSettingButton.disabled = setting_texts.length <= 1;
});
// 检查是否只剩一个设置,如果是,则禁用删除按钮
if (setting_texts.length <= 1) {
deleteSettingButton.disabled = true;
}
const modeSettingButton = document.createElement('button');
modeSettingButton.textContent = menuModeText();
modeSettingButton.className = 'settings-button';
modeSettingButton.addEventListener('click', () => {
switchMode();
modeSettingButton.textContent = menuModeText();
});
submitButton.addEventListener('click', () => {
const selectedSettingIndex = settingsDropdown.selectedIndex;
if (typeof setting_texts[selectedSettingIndex] === 'undefined') {
console.error("Trying to save a setting that doesn't exist.");
return;
}
setting_texts[selectedSettingIndex] = textarea.value;
localStorage.setItem(LSID_SETTING_TEXTS, JSON.stringify(setting_texts));
localStorage.setItem(LSID_SETTING_CURRENT_INDEX, selectedSettingIndex.toString());
current_setting_text = textarea.value;
setting_current_index = selectedSettingIndex;
if (current_setting_text) {
updateMenuItems();
processAllElements();
}
modal.remove();
});
const settingConfirm = createConfirmModal(modal);
cancelButton.addEventListener('click', () => {
closeModal(settingConfirm, modal);
});
const buttonsLeft = document.createElement('span');
const buttonsRight = document.createElement('span');
buttonsLeft.appendChild(newSettingButton);
buttonsLeft.appendChild(deleteSettingButton);
buttonsRight.appendChild(modeSettingButton);
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'buttonsContainer';
buttonsContainer.appendChild(buttonsLeft);
buttonsContainer.appendChild(buttonsRight);
modalContent.appendChild(settingsDropdown);
modalContent.appendChild(buttonsContainer);
modalContent.appendChild(textarea);
modalContent.appendChild(submitButton);
modalContent.appendChild(cancelButton);
modal.appendChild(modalContent);
modal.appendChild(settingConfirm);
document.body.appendChild(modal);
}
function showAddTemplateModal(selectText) {
let choosedIndex = setting_current_index;
const modal = document.createElement('div');
modal.className = 'settings-modal';
const labelText = document.createElement('p');
labelText.textContent = '将内容添加到选定功能组';
const modalContent = document.createElement('div');
modalContent.className = 'settings-content';
const inputField = document.createElement('input');
inputField.type = 'text';
inputField.placeholder = '这里填写会出现在菜单上的功能名称';
inputField.className = 'settings-input';
const labelInstruction = document.createElement('p');
labelInstruction.textContent = '下方是 prompt 模版,使用 {__PLACE_HOLDER__} 作为占位符。'
const textarea = document.createElement('textarea');
textarea.className = 'settings-textarea';
textarea.value = selectText;
const submitButton = document.createElement('button');
submitButton.className = 'settings-button';
submitButton.textContent = '添加到选定功能组';
submitButton.disabled = true;
function updateSubmitButton() {
if (inputField.value.trim() == '' || textarea.value.trim() == '') {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
}
inputField.addEventListener('input', updateSubmitButton);
textarea.addEventListener('input', updateSubmitButton);
const cancelButton = document.createElement('button');
cancelButton.className = 'settings-button';
cancelButton.textContent = '取消';
const settingsDropdown = document.createElement('select');
settingsDropdown.className = 'settings-dropdown';
setting_texts.forEach((text, index) => {
const option = document.createElement('option');
option.value = index;
option.text = text.split('\n')[0]; // Assuming the first line is a title or identifier
settingsDropdown.appendChild(option);
});
settingsDropdown.selectedIndex = choosedIndex;
settingsDropdown.addEventListener('change', (e) => {
choosedIndex = e.target.value;
});
submitButton.addEventListener('click', () => {
choosedIndex = settingsDropdown.selectedIndex;
if (typeof setting_texts[choosedIndex] === 'undefined') {
console.error("Trying to save a setting that doesn't exist.");
return;
}
let original = setting_texts[choosedIndex];
let toAdd = '\n🪄🪄🪄🪄🪄🪄🪄🪄\n' + inputField.value + '\n' + textarea.value + '\n';
// 找到 original 中第一个 📖📖📖📖📖📖📖📖 的位置,在此之前插入 textarea.value
// 如果没有找到,则在末尾插入
let index = original.indexOf('📖📖📖📖📖📖📖📖');
if (index >= 0) {
setting_texts[choosedIndex] = original.slice(0, index) + toAdd + original.slice(index);
} else {
setting_texts[choosedIndex] = original + toAdd;
}
localStorage.setItem(LSID_SETTING_TEXTS, JSON.stringify(setting_texts));
current_setting_text = setting_texts[setting_current_index];
if (current_setting_text) {
updateMenuItems();
processAllElements();
}
modal.remove();
});
const settingConfirm = createConfirmModal(modal);
cancelButton.addEventListener('click', () => {
closeModal(settingConfirm, modal);
});
modalContent.appendChild(labelText);
modalContent.appendChild(settingsDropdown);
modalContent.appendChild(inputField);
modalContent.appendChild(labelInstruction);
modalContent.appendChild(textarea);
modalContent.appendChild(submitButton);
modalContent.appendChild(cancelButton);
modal.appendChild(modalContent);
modal.appendChild(settingConfirm);
document.body.appendChild(modal);
}
let menus = [];
let clicks = []
function parseMenus(settingsText) {
const buttonData = settingsText.split("🪄🪄🪄🪄🪄🪄🪄🪄").slice(1);
buttonData.forEach(data => {
const lines = data.trim().split("\n");
if (lines.length >= 2) {
const name = lines[0];
const content = lines.slice(1).join("\n");
menus.push([name, content]);
}
});
}
function parseClicks(settingText) {
// 根据 📖📖📖📖📖📖📖📖 分割设置文件,并移除首尾的空值
const configArray = settingText.split('📖📖📖📖📖📖📖📖').filter(Boolean);
let templates = []
// 遍历每个设置
configArray.forEach(config => {
// 按行分割配置
const lines = config.trim().split('\n');
// 第一行是政策表达式
const regExpression = new RegExp(lines[0], 'g');
// 后续行组成替换模板
const template = lines.slice(1).join('\n');
templates.push(template);
// 逐一检查 templates 中是否有能够匹配 regExpression 的模板
// 如果有,新的内容需要添加在该 templates 前面,以避免被之前的模板匹配
let index = clicks.findIndex(c => c[1].match(regExpression));
if (index >=0) {
// 如果有匹配的模板,则将新的[regExpression, template] 插入到该模板前面
clicks.splice(index, 0, [regExpression, template]);
} else {
// 如果没有匹配的模板,则将新的[regExpression, template] 添加到数组末尾
clicks.push([regExpression, template]);
}
});
console.log(clicks);
}
function parseSettingsText(settingsText) {
menus.length = 0; // Clear the existing array
clicks.length = 0; // Clear the existing array
let splitted = settingsText.split("📖📖📖📖📖📖📖📖")
if (splitted.length < 2) {
parseMenus(settingsText);
} else {
parseMenus(splitted[0]);
parseClicks(splitted.slice(1).join("📖📖📖📖📖📖📖📖"));
}
}
////////////////////////// Main //////////////////////////
initContextMenu();
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) {
startListening();
}
}
});
observer.observe({ entryTypes: ['resource'] });
功能组模版说明
目前预置了英语学习功能组。 欢迎大家补充新的功能组到 tool_templates 下。 如果你想添加新的功能,只需要把功能组模版粘贴到设置页面里。
文件格式说明:
* 第一行是功能组名称,紧跟着它可以是一些使用说明
* 用 🪄🪄🪄🪄🪄🪄🪄🪄 分隔各个功能组按钮(之所以选择这么奇葩的方式,是为了在一个文件里能够方便写多个 prompt。prompt 啥格式都有,用常规的 json yaml 之类的,写转义字符会写到怀疑人生)
* 🪄🪄🪄🪄🪄🪄🪄🪄 分隔符之后的第一行是按钮名称,然后跟着的就是 prompt 具体内容。
* prompt 中的 {\_\_PLACE_HOLDER\_\_} 会被鼠标选中的页面文字替代掉。
正则匹配的高级功能:
* 📖📖📖📖📖📖📖📖 分隔符用于将 GPT 输出的内容转化成可以直接点击的项。📖 一定要在 🪄 之后。
* 📖📖📖📖📖📖📖📖 之后的第一行是用于匹配文本的正则表达式,然后跟着的就是 prompt 的具体内容。
* 同样,prompt 中的 {\_\_PLACE_HOLDER\_\_} 会被鼠标选中的页面文字替代掉。通过 📖 方式定义的内容会被直接发送,没有编辑选项。
联系作者
作者有个公众号:南瓜博士,欢迎关注。
本插件感谢豆爸开发的 关联学习工具 给到的启发。