🤤Claude - Prompt便签

一个帮助用户在Claude原生网页添加可移动且大小可调的便签,用于快速选择和添加prompt的脚本。

  1. // ==UserScript==
  2. // @name 🤤Claude - Prompt便签
  3. // @version 1.2
  4. // @description 一个帮助用户在Claude原生网页添加可移动且大小可调的便签,用于快速选择和添加prompt的脚本。
  5. // @author futureo0
  6. // @license MIT
  7. // @require https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.6.4.min.js
  8. // @match https://claude.ai/*
  9. // @match https://claude.ai/chats/*
  10. // @run-at document-idle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @namespace https://gf.qytechs.cn/users/1242018
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. // 等待页面完全加载并确保输入框存在
  20. function waitForElement(selector) {
  21. return new Promise(resolve => {
  22. if (document.querySelector(selector)) {
  23. return resolve(document.querySelector(selector));
  24. }
  25.  
  26. const observer = new MutationObserver(() => {
  27. if (document.querySelector(selector)) {
  28. resolve(document.querySelector(selector));
  29. observer.disconnect();
  30. }
  31. });
  32.  
  33. observer.observe(document.body, {
  34. childList: true,
  35. subtree: true
  36. });
  37. });
  38. }
  39.  
  40. // 获取当前活动的输入区域
  41. function getActiveInputField() {
  42. // 获取当前焦点元素
  43. let activeElement = document.activeElement;
  44.  
  45. // 如果是textarea或可编辑div,直接返回
  46. if ((activeElement.tagName.toLowerCase() === 'textarea') ||
  47. (activeElement.classList.contains('ProseMirror') && activeElement.isContentEditable)) {
  48. return activeElement;
  49. }
  50.  
  51. // 获取所有可能的输入区域
  52. const allInputs = Array.from(document.querySelectorAll('textarea, .ProseMirror[contenteditable="true"]'))
  53. .filter(input => input.offsetParent !== null);
  54.  
  55. // 检查是否有输入框包含当前选区
  56. const sel = window.getSelection();
  57. if (sel.rangeCount > 0) {
  58. const range = sel.getRangeAt(0);
  59. for (const input of allInputs) {
  60. if (input.contains(range.commonAncestorContainer)) {
  61. return input;
  62. }
  63. }
  64. }
  65.  
  66. // 如果没有找到,返回最后一个可见的输入框
  67. return allInputs[allInputs.length - 1] || null;
  68. }
  69.  
  70. function insertAtCursor(myField, myValue) {
  71. // 确保输入区域获得焦点
  72. myField.focus();
  73.  
  74. if (myField.classList.contains('ProseMirror') && myField.isContentEditable) {
  75. // 处理 ProseMirror 编辑器
  76. const sel = window.getSelection();
  77. if (sel.rangeCount > 0) {
  78. const range = sel.getRangeAt(0);
  79.  
  80. // 确保选区在目标输入框内
  81. if (myField.contains(range.commonAncestorContainer)) {
  82. // 删除当前选中内容
  83. range.deleteContents();
  84.  
  85. // 创建并插入文本节点
  86. const textNode = document.createTextNode(myValue);
  87. range.insertNode(textNode);
  88.  
  89. // 移动光标到插入文本之后
  90. range.setStartAfter(textNode);
  91. range.setEndAfter(textNode);
  92. sel.removeAllRanges();
  93. sel.addRange(range);
  94. } else {
  95. // 如果选区不在输入框内,在末尾插入
  96. const newRange = document.createRange();
  97. const lastChild = myField.lastChild;
  98.  
  99. if (lastChild) {
  100. if (lastChild.nodeType === Node.TEXT_NODE) {
  101. newRange.setStartAfter(lastChild);
  102. newRange.setEndAfter(lastChild);
  103. } else {
  104. newRange.selectNodeContents(myField);
  105. newRange.collapse(false);
  106. }
  107. } else {
  108. newRange.selectNodeContents(myField);
  109. newRange.collapse(false);
  110. }
  111.  
  112. const textNode = document.createTextNode(myValue);
  113. newRange.insertNode(textNode);
  114.  
  115. // 移动光标到插入文本之后
  116. newRange.setStartAfter(textNode);
  117. newRange.setEndAfter(textNode);
  118. sel.removeAllRanges();
  119. sel.addRange(newRange);
  120. }
  121. }
  122. } else if (myField.tagName.toLowerCase() === 'textarea') {
  123. // 处理普通 textarea
  124. const startPos = myField.selectionStart;
  125. const endPos = myField.selectionEnd;
  126.  
  127. // 在光标位置插入文本
  128. myField.value = myField.value.substring(0, startPos) +
  129. myValue +
  130. myField.value.substring(endPos);
  131.  
  132. // 更新光标位置
  133. myField.selectionStart = myField.selectionEnd = startPos + myValue.length;
  134. }
  135.  
  136. // 触发输入事件
  137. const event = new Event('input', { bubbles: true });
  138. myField.dispatchEvent(event);
  139. }
  140.  
  141. // 初始化便签
  142. async function initStickyNote() {
  143. // 等待输入框加载完成
  144. await waitForElement('.ProseMirror[contenteditable="true"]');
  145.  
  146. // 检查是否已存在便签
  147. if (document.getElementById('stickyNoteContainer')) {
  148. return;
  149. }
  150.  
  151. const stickyNoteHtml = `
  152. <div id="stickyNoteContainer" style="position: fixed; top: 40px; left: calc(100vw - 350px); width: 300px; min-height: 300px; background-color: lightyellow; border: 1px solid black; padding-bottom: 10px; box-shadow: 3px 3px 5px rgba(0,0,0,0.2); z-index: 10000; resize: both; overflow: auto; color: black;">
  153. <div id="stickyNoteHeader" style="cursor: move; background-color: #ddd; height: 10px; width: 100%;"></div>
  154. <input type="text" id="newPromptInput" placeholder="输入新prompt" style="width: calc(100% - 20px); margin: 10px;">
  155. <button id="savePromptButton" style="width: calc(100% - 20px); margin: 0 10px;">保存</button>
  156. <div id="promptsList" style="margin: 5px 10px;"></div>
  157. </div>
  158. `;
  159.  
  160. // 将便签添加到body中
  161. $('body').append(stickyNoteHtml);
  162.  
  163. // 初始化拖拽功能
  164. dragElement(document.getElementById("stickyNoteContainer"), document.getElementById("stickyNoteHeader"));
  165.  
  166. // 加载已保存的prompts
  167. loadPrompts();
  168.  
  169. // 绑定保存按钮事件
  170. $('#savePromptButton').click(function() {
  171. const newPrompt = $('#newPromptInput').val().trim();
  172. if(newPrompt) {
  173. addPromptToStickyNote(newPrompt);
  174. savePrompt(newPrompt);
  175. $('#newPromptInput').val('');
  176. }
  177. });
  178.  
  179. // 绑定回车键保存
  180. $('#newPromptInput').keypress(function(e) {
  181. if(e.which == 13) {
  182. $('#savePromptButton').click();
  183. }
  184. });
  185. }
  186.  
  187. function addPromptToStickyNote(promptText) {
  188. const promptHtml = `
  189. <div style="display: flex; align-items: center; margin-bottom: 10px;">
  190. <button class="deletePromptButton" style="cursor: pointer; background: none; border: none; color: grey; margin-right: 5px;">×</button>
  191. <div style="cursor: pointer; flex-grow: 1; word-break: break-all;">${promptText}</div>
  192. </div>
  193. `;
  194. $('#promptsList').prepend(promptHtml);
  195.  
  196. // 删除按钮事件
  197. $('#promptsList .deletePromptButton:first').click(function(e) {
  198. e.stopPropagation();
  199. const promptText = $(this).siblings('div').text();
  200. removePromptByText(promptText);
  201. $(this).parent().remove();
  202. });
  203.  
  204. // 使用 mousedown 事件来预防焦点丢失
  205. $('#promptsList div:first').on('mousedown', function(e) {
  206. e.preventDefault();
  207. const inputField = getActiveInputField();
  208. if (inputField) {
  209. insertAtCursor(inputField, promptText);
  210. }
  211. });
  212. }
  213.  
  214. function loadPrompts() {
  215. const prompts = JSON.parse(GM_getValue('prompts', '[]'));
  216. $('#promptsList').empty();
  217. prompts.forEach(prompt => {
  218. addPromptToStickyNote(prompt);
  219. });
  220. }
  221.  
  222. function removePromptByText(promptText) {
  223. let prompts = JSON.parse(GM_getValue('prompts', '[]'));
  224. const promptIndex = prompts.indexOf(promptText);
  225. if (promptIndex !== -1) {
  226. prompts.splice(promptIndex, 1);
  227. GM_setValue('prompts', JSON.stringify(prompts));
  228. }
  229. }
  230.  
  231. function savePrompt(promptText) {
  232. let prompts = JSON.parse(GM_getValue('prompts', '[]'));
  233. if (!prompts.includes(promptText)) {
  234. prompts.push(promptText);
  235. GM_setValue('prompts', JSON.stringify(prompts));
  236. }
  237. }
  238.  
  239. function dragElement(elmnt, header) {
  240. var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
  241.  
  242. header.onmousedown = dragMouseDown;
  243.  
  244. function dragMouseDown(e) {
  245. if (e.target === header) {
  246. e = e || window.event;
  247. e.preventDefault();
  248. pos3 = e.clientX;
  249. pos4 = e.clientY;
  250. document.onmouseup = closeDragElement;
  251. document.onmousemove = elementDrag;
  252. }
  253. }
  254.  
  255. function elementDrag(e) {
  256. e = e || window.event;
  257. e.preventDefault();
  258. pos1 = pos3 - e.clientX;
  259. pos2 = pos4 - e.clientY;
  260. pos3 = e.clientX;
  261. pos4 = e.clientY;
  262.  
  263. elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
  264. elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
  265. }
  266.  
  267. function closeDragElement() {
  268. document.onmouseup = null;
  269. document.onmousemove = null;
  270. }
  271. }
  272.  
  273. // 页面加载完成后初始化便签
  274. window.addEventListener('load', function() {
  275. setTimeout(initStickyNote, 1000);
  276. });
  277.  
  278. // 监听URL变化,在切换页面时重新初始化便签
  279. let lastUrl = location.href;
  280. new MutationObserver(() => {
  281. const url = location.href;
  282. if (url !== lastUrl) {
  283. lastUrl = url;
  284. setTimeout(initStickyNote, 1000);
  285. }
  286. }).observe(document, {subtree: true, childList: true});
  287.  
  288. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址