Prompt Manager (Fixed Vertical Drag with Copy & Close)

在AI网站上保存并快速使用 Prompts,同时支持拖动按钮及位置保存 —— 修复按钮只能横向拖动的问题,增大关闭按钮点击区域并上移碰撞箱,复制后显示成功并自动关闭

  1. // ==UserScript==
  2. // @name Prompt Manager (Fixed Vertical Drag with Copy & Close)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.7.5
  5. // @description 在AI网站上保存并快速使用 Prompts,同时支持拖动按钮及位置保存 —— 修复按钮只能横向拖动的问题,增大关闭按钮点击区域并上移碰撞箱,复制后显示成功并自动关闭
  6. // @author schweigen
  7. // @match https://chatgpt.com/*
  8. // @match https://claude.ai/*
  9. // @match https://chat.deepseek.com/*
  10. // @match https://www.perplexity.ai/*
  11. // @match https://chat.mistral.ai/*
  12. // @match https://app.nextchat.dev/*
  13. // @match https://chat01.ai/*
  14. // @match https://you.com/*
  15. // @match https://chatgpt.aicnm.cc/*
  16. // @match https://chatshare.xyz/*
  17. // @match https://chat.biggraph.net/*
  18. // @match https://grok.com/*
  19. // @grant GM_addStyle
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @grant GM_registerMenuCommand
  23. // @grant GM_deleteValue
  24. // @license MIT
  25. // ==/UserScript==
  26.  
  27. (function() {
  28. 'use strict';
  29.  
  30. // === 用户可编辑的 Prompts 列表 ===
  31. const prompts = [
  32. {
  33. title: "",
  34. content: ``
  35. },
  36. {
  37. title: "",
  38. content: ``
  39. },
  40. {
  41. title: "",
  42. content: ``
  43. },
  44. {
  45. title: "",
  46. content: ``
  47. },
  48. {
  49. title: "",
  50. content: ``
  51. },
  52. {
  53. title: "",
  54. content: ``
  55. },
  56. {
  57. title: "",
  58. content: ``
  59. },
  60. {
  61. title: "",
  62. content: ``
  63. },
  64. {
  65. title: "",
  66. content: ``
  67. },
  68. {
  69. title: "",
  70. content: ``
  71. },
  72. ];
  73.  
  74. // 添加必要的样式
  75. GM_addStyle(`
  76. /* Prompt Manager 容器样式 */
  77. #prompt-manager {
  78. position: fixed !important;
  79. top: 80px !important;
  80. right: 20px !important;
  81. width: 350px !important;
  82. max-height: 80vh !important;
  83. overflow-y: auto !important;
  84. overflow-x: visible !important;
  85. background: #ffffff !important;
  86. border: 1px solid #e1e4e8 !important;
  87. border-radius: 12px !important;
  88. box-shadow: 0 4px 16px rgba(0,0,0,0.1) !important;
  89. z-index: 2147483647 !important;
  90. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
  91. display: block !important;
  92. color: #24292e !important;
  93. opacity: 1 !important;
  94. visibility: visible !important;
  95. }
  96.  
  97. #prompt-manager.hidden {
  98. display: none !important;
  99. }
  100.  
  101. /* 标题样式 */
  102. #prompt-manager h2 {
  103. margin: 0 !important;
  104. padding: 16px !important;
  105. background: #2c3e50 !important;
  106. color: #ffffff !important;
  107. border-radius: 12px 12px 0 0 !important;
  108. text-align: center !important;
  109. font-size: 18px !important;
  110. font-weight: 600 !important;
  111. position: relative !important;
  112. }
  113.  
  114. /* 关闭按钮样式(碰撞箱上移) */
  115. #close-prompt-btn {
  116. position: absolute !important;
  117. top: -10px !important; /* 向上移动显示区域 */
  118. right: 0 !important;
  119. padding: 10px 16px !important;
  120. cursor: pointer !important;
  121. font-size: 20px !important;
  122. color: #ffffff !important;
  123. user-select: none !important;
  124. }
  125.  
  126. /* Prompt 项样式 */
  127. .prompt-item {
  128. border-bottom: 1px solid #e1e4e8 !important;
  129. padding: 12px 16px !important;
  130. position: relative !important;
  131. transition: all 0.2s ease !important;
  132. background: #ffffff !important;
  133. }
  134.  
  135. .prompt-item:hover {
  136. background: #f6f8fa !important;
  137. }
  138.  
  139. .prompt-title {
  140. font-weight: 500 !important;
  141. cursor: pointer !important;
  142. position: relative !important;
  143. display: flex !important;
  144. justify-content: space-between !important;
  145. align-items: center !important;
  146. color: #2c3e50 !important;
  147. }
  148.  
  149. .prompt-content {
  150. display: none !important;
  151. margin-top: 8px !important;
  152. white-space: pre-wrap !important;
  153. background: #f8f9fa !important;
  154. padding: 12px !important;
  155. border-radius: 6px !important;
  156. cursor: pointer !important;
  157. transition: background 0.2s ease !important;
  158. color: #2c3e50 !important;
  159. border: 1px solid #e1e4e8 !important;
  160. }
  161.  
  162. .prompt-content:hover {
  163. background: #edf2f7 !important;
  164. }
  165.  
  166. /* 复制按钮样式 */
  167. .copy-button {
  168. background: #3498db !important;
  169. color: #ffffff !important;
  170. border: none !important;
  171. padding: 6px 12px !important;
  172. border-radius: 4px !important;
  173. cursor: pointer !important;
  174. font-size: 12px !important;
  175. margin-left: 10px !important;
  176. transition: all 0.2s ease !important;
  177. }
  178.  
  179. .copy-button:hover {
  180. background: #2980b9 !important;
  181. transform: translateY(-1px) !important;
  182. }
  183.  
  184. /* Toggle 按钮样式 */
  185. #toggle-prompt-btn {
  186. position: fixed !important;
  187. top: 60px !important;
  188. right: 20px !important;
  189. width: 40px !important;
  190. height: 40px !important;
  191. background: #3498db !important;
  192. color: #ffffff !important;
  193. border: none !important;
  194. border-radius: 50% !important;
  195. cursor: pointer !important;
  196. font-size: 20px !important;
  197. display: flex !important;
  198. align-items: center !important;
  199. justify-content: center !important;
  200. z-index: 2147483647 !important;
  201. transition: all 0.2s ease !important;
  202. box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
  203. opacity: 1 !important;
  204. visibility: visible !important;
  205. }
  206.  
  207. #toggle-prompt-btn:hover {
  208. background: #2980b9 !important;
  209. transform: translateY(-1px) !important;
  210. }
  211.  
  212. /* 复制成功提示样式 */
  213. #copy-success {
  214. position: fixed !important;
  215. top: 100px !important;
  216. right: 20px !important;
  217. background: #2ecc71 !important;
  218. color: #ffffff !important;
  219. padding: 8px 16px !important;
  220. border-radius: 6px !important;
  221. opacity: 0 !important;
  222. transition: opacity 0.3s ease !important;
  223. z-index: 2147483647 !important;
  224. font-size: 14px !important;
  225. box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
  226. }
  227.  
  228. /* 内部成功提示样式 */
  229. .inner-success {
  230. background: #2ecc71 !important;
  231. color: #ffffff !important;
  232. padding: 8px 12px !important;
  233. margin-top: 8px !important;
  234. border-radius: 6px !important;
  235. text-align: center !important;
  236. font-size: 14px !important;
  237. display: none !important;
  238. }
  239.  
  240. /* 搜索输入框样式 */
  241. #search-input {
  242. width: calc(100% - 32px) !important;
  243. padding: 10px 12px !important;
  244. margin: 16px !important;
  245. border: 1px solid #e1e4e8 !important;
  246. border-radius: 6px !important;
  247. background: #f8f9fa !important;
  248. color: #2c3e50 !important;
  249. font-size: 14px !important;
  250. transition: all 0.2s ease !important;
  251. }
  252.  
  253. #search-input:focus {
  254. outline: none !important;
  255. border-color: #3498db !important;
  256. box-shadow: 0 0 0 2px rgba(52,152,219,0.2) !important;
  257. }
  258.  
  259. #search-input::placeholder {
  260. color: #95a5a6 !important;
  261. }
  262. `);
  263.  
  264. // 确保DOM加载完成后再创建元素
  265. function createElements() {
  266. // 创建 Toggle 按钮
  267. const toggleBtn = document.createElement('button');
  268. toggleBtn.id = 'toggle-prompt-btn';
  269. toggleBtn.title = '隐藏/显示 Prompt Manager';
  270. toggleBtn.innerHTML = '☰';
  271. document.body.appendChild(toggleBtn);
  272.  
  273. // 如果用户之前拖动过,则恢复按钮保存的位置
  274. const savedX = GM_getValue('toggleBtnX', null);
  275. const savedY = GM_getValue('toggleBtnY', null);
  276. if (savedX !== null && savedY !== null) {
  277. toggleBtn.style.setProperty('left', savedX + 'px', 'important');
  278. toggleBtn.style.setProperty('top', savedY + 'px', 'important');
  279. toggleBtn.style.setProperty('right', 'auto', 'important');
  280. }
  281.  
  282. // 创建 Prompt Manager 容器,增加了关闭叉号
  283. const manager = document.createElement('div');
  284. manager.id = 'prompt-manager';
  285. manager.classList.add('hidden'); // 默认隐藏
  286. manager.innerHTML = `
  287. <h2>
  288. Prompts
  289. <span id="close-prompt-btn" title="关闭">×</span>
  290. </h2>
  291. <input type="text" id="search-input" placeholder="搜索 Prompts...">
  292. <div id="prompt-list"></div>
  293. `;
  294. document.body.appendChild(manager);
  295.  
  296. // 为关闭叉号添加点击事件
  297. const closeBtn = document.getElementById('close-prompt-btn');
  298. closeBtn.addEventListener('click', () => {
  299. manager.classList.add('hidden');
  300. });
  301.  
  302. // 创建复制成功提示
  303. const copySuccess = document.createElement('div');
  304. copySuccess.id = 'copy-success';
  305. copySuccess.textContent = '复制成功';
  306. document.body.appendChild(copySuccess);
  307.  
  308. // 创建一个 Prompt 项
  309. function createPromptItem(prompt, index) {
  310. const item = document.createElement('div');
  311. item.className = 'prompt-item';
  312.  
  313. const title = document.createElement('div');
  314. title.className = 'prompt-title';
  315.  
  316. const titleText = document.createElement('span');
  317. titleText.textContent = prompt.title || "无标题 Prompt";
  318.  
  319. const copyTitleBtn = document.createElement('button');
  320. copyTitleBtn.className = 'copy-button';
  321. copyTitleBtn.textContent = '复制';
  322. copyTitleBtn.title = '复制整个 Prompt 内容';
  323.  
  324. // 创建内部成功提示元素
  325. const innerSuccess = document.createElement('div');
  326. innerSuccess.className = 'inner-success';
  327. innerSuccess.textContent = '复制成功';
  328. innerSuccess.style.display = 'none';
  329.  
  330. copyTitleBtn.onclick = (e) => {
  331. e.stopPropagation();
  332. if (prompt.content) {
  333. copyToClipboard(prompt.content, item);
  334. } else {
  335. showInnerSuccess(item, '内容为空,无法复制。');
  336. }
  337. };
  338.  
  339. // 仅添加标题和复制按钮
  340. title.appendChild(titleText);
  341. title.appendChild(copyTitleBtn);
  342.  
  343. const content = document.createElement('div');
  344. content.className = 'prompt-content';
  345. content.textContent = prompt.content || "无内容 Prompt";
  346.  
  347. content.addEventListener('click', () => {
  348. if (prompt.content) {
  349. copyToClipboard(prompt.content, item);
  350. } else {
  351. showInnerSuccess(item, '内容为空,无法复制。');
  352. }
  353. });
  354.  
  355. // 仅添加点击切换内容显示
  356. title.addEventListener('click', () => {
  357. const isVisible = content.style.display === 'block';
  358. content.style.display = isVisible ? 'none' : 'block';
  359. });
  360.  
  361. item.appendChild(title);
  362. item.appendChild(content);
  363. item.appendChild(innerSuccess); // 添加内部成功提示
  364.  
  365. return item;
  366. }
  367.  
  368. // 渲染 Prompts 列表
  369. function renderPrompts(filter = '') {
  370. const promptList = document.getElementById('prompt-list');
  371. promptList.innerHTML = '';
  372. const filtered = prompts.filter(p =>
  373. (p.title && p.title.toLowerCase().includes(filter.toLowerCase())) ||
  374. (p.content && p.content.toLowerCase().includes(filter.toLowerCase()))
  375. );
  376. filtered.forEach((prompt, index) => {
  377. const item = createPromptItem(prompt, index);
  378. promptList.appendChild(item);
  379. });
  380. }
  381.  
  382. // 复制到剪贴板并显示成功提示
  383. function copyToClipboard(text, promptItem) {
  384. navigator.clipboard.writeText(text).then(() => {
  385. showInnerSuccess(promptItem, '复制成功');
  386. }).catch(err => {
  387. console.error('复制失败: ', err);
  388. showInnerSuccess(promptItem, '复制失败,请手动复制。');
  389. });
  390. }
  391.  
  392. // 显示成功提示并立即关闭面板
  393. function showInnerSuccess(promptItem, message = '复制成功') {
  394. // 直接关闭面板,不显示内部提示
  395. document.getElementById('prompt-manager').classList.add('hidden');
  396.  
  397. // 在外部显示一个简短的提示
  398. showCopySuccess(message);
  399. }
  400.  
  401. // 显示复制成功提示(保留旧函数以兼容)
  402. function showCopySuccess(message = '复制成功') {
  403. copySuccess.textContent = message;
  404. copySuccess.style.opacity = '1';
  405. setTimeout(() => {
  406. copySuccess.style.opacity = '0';
  407. }, 1500);
  408. }
  409.  
  410. // ======= 以下为拖拽功能 =======
  411. let isDragging = false, justDragged = false, startX, startY, origLeft, origTop;
  412.  
  413. toggleBtn.addEventListener('mousedown', function(e) {
  414. if (e.button !== 0) return; // 仅响应鼠标左键
  415. isDragging = false;
  416. startX = e.clientX;
  417. startY = e.clientY;
  418. // 获取当前按钮的位置(相对于视口)
  419. const rect = toggleBtn.getBoundingClientRect();
  420. origLeft = rect.left;
  421. origTop = rect.top;
  422.  
  423. function onMouseMove(e) {
  424. const dx = e.clientX - startX;
  425. const dy = e.clientY - startY;
  426. if (!isDragging) {
  427. // 超过 5px 视为拖拽操作
  428. if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
  429. isDragging = true;
  430. }
  431. }
  432. if (isDragging) {
  433. // 使用 setProperty 带上 'important' 以覆盖样式中的 !important
  434. toggleBtn.style.setProperty('left', (origLeft + dx) + 'px', 'important');
  435. toggleBtn.style.setProperty('top', (origTop + dy) + 'px', 'important');
  436. toggleBtn.style.setProperty('right', 'auto', 'important');
  437. e.preventDefault();
  438. }
  439. }
  440.  
  441. function onMouseUp(e) {
  442. document.removeEventListener('mousemove', onMouseMove);
  443. document.removeEventListener('mouseup', onMouseUp);
  444. if (isDragging) {
  445. justDragged = true;
  446. // 保存新位置
  447. const newLeft = parseInt(toggleBtn.style.left, 10);
  448. const newTop = parseInt(toggleBtn.style.top, 10);
  449. GM_setValue('toggleBtnX', newLeft);
  450. GM_setValue('toggleBtnY', newTop);
  451. }
  452. }
  453.  
  454. document.addEventListener('mousemove', onMouseMove);
  455. document.addEventListener('mouseup', onMouseUp);
  456. });
  457.  
  458. // 修改点击事件,避免拖拽后触发点击
  459. toggleBtn.addEventListener('click', (e) => {
  460. if (justDragged) {
  461. justDragged = false;
  462. return;
  463. }
  464. manager.classList.toggle('hidden');
  465. });
  466.  
  467. // Toggle 按钮快捷键显示/隐藏 Prompt Manager (Ctrl/Command + O)
  468. document.addEventListener('keydown', (e) => {
  469. const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
  470. const modifier = isMac ? e.metaKey : e.ctrlKey;
  471.  
  472. if (modifier && e.key.toLowerCase() === 'o') {
  473. e.preventDefault();
  474. manager.classList.toggle('hidden');
  475. }
  476. });
  477.  
  478. // 搜索 Prompts
  479. const searchInput = document.getElementById('search-input');
  480. searchInput.addEventListener('input', () => {
  481. renderPrompts(searchInput.value);
  482. });
  483.  
  484. // 初始渲染
  485. renderPrompts();
  486. }
  487.  
  488. // 确保DOM加载完成后再创建元素
  489. if (document.readyState === 'loading') {
  490. document.addEventListener('DOMContentLoaded', createElements);
  491. } else {
  492. createElements();
  493. }
  494.  
  495. // 每隔一秒检查一次是否需要重新创建元素(用于处理某些网站的动态加载)
  496. let checkInterval = setInterval(() => {
  497. if (!document.getElementById('toggle-prompt-btn')) {
  498. createElements();
  499. }
  500. }, 1000);
  501.  
  502. // 5分钟后停止检查,以避免无限循环
  503. setTimeout(() => {
  504. clearInterval(checkInterval);
  505. }, 300000); // 5分钟
  506.  
  507. // ======= 添加油猴菜单命令,用于重置按钮默认位置 =======
  508. GM_registerMenuCommand("重置按钮默认位置", () => {
  509. GM_deleteValue('toggleBtnX');
  510. GM_deleteValue('toggleBtnY');
  511. const toggleBtn = document.getElementById('toggle-prompt-btn');
  512. if (toggleBtn) {
  513. toggleBtn.style.setProperty('top', '60px', 'important');
  514. toggleBtn.style.setProperty('right', '20px', 'important');
  515. toggleBtn.style.removeProperty('left');
  516. }
  517. });
  518. })();