Prompt Manager

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

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