Prompt Manager

Fixed focus issue with search, import, and export (search below list)

  1. // ==UserScript==
  2. // @name Prompt Manager
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Fixed focus issue with search, import, and export (search below list)
  6. // @author yowaimono
  7. // @match https://grok.com/chat/*
  8. // @match https://askmanyai.cn/chat/*
  9. // @match https://chat.deepseek.com/a/chat/*
  10. // @match https://yuanbao.tencent.com/*
  11. // @match https://kimi.moonshot.cn/chat/*
  12. // @match https://www.wenxiaobai.com/chat/*
  13. // @match https://chatgpt.com/*
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
  15. // @grant none
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. // 配置数据
  23. const STORAGE_KEY = 'promptManagerData';
  24. let prompts = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
  25. let filteredPrompts = [...prompts]; // 用于存储搜索结果
  26. let currentEditIndex = null;
  27. let isMinimized = false;
  28. let lastFocusedElement = null; // 新增:记录最后聚焦的元素
  29.  
  30. // 创建主容器
  31. const container = document.createElement('div');
  32. container.id = 'prompt-manager';
  33. container.innerHTML = `
  34. <div class="pm-header">
  35. <h3>提示词管理</h3>
  36. <div class="pm-header-buttons">
  37. <button class="pm-icon-btn" id="pm-export">
  38. <span class="pm-icon">⬇</span>
  39. </button>
  40. <button class="pm-icon-btn" id="pm-import">
  41. <span class="pm-icon">⬆</span>
  42. </button>
  43. <button class="pm-icon-btn" id="pm-minimize">
  44. <span class="pm-icon">-</span>
  45. </button>
  46. <button class="pm-icon-btn" id="pm-add">
  47. <span class="pm-icon">+</span>
  48. </button>
  49. </div>
  50. </div>
  51. <div class="pm-list" id="pm-list"></div>
  52. <div class="pm-search-container">
  53. <input type="text" id="pm-search" placeholder="搜索提示词" class="pm-input">
  54. </div>
  55.  
  56. <div class="pm-modal" id="pm-modal">
  57. <div class="pm-modal-content">
  58. <div class="pm-modal-header">
  59. <h4>${currentEditIndex !== null ? '编辑提示词' : '新建提示词'}</h4>
  60. <button class="pm-icon-btn" id="pm-close">
  61. <span class="pm-icon">×</span>
  62. </button>
  63. </div>
  64. <div class="pm-modal-body">
  65. <input type="text" id="pm-title" placeholder="请输入标题" class="pm-input">
  66. <textarea id="pm-content" placeholder="请输入内容" class="pm-textarea"></textarea>
  67. </div>
  68. <div class="pm-modal-footer">
  69. <button class="pm-btn pm-primary" id="pm-save">保存</button>
  70. <button class="pm-btn" id="pm-cancel">取消</button>
  71. </div>
  72. </div>
  73. </div>
  74. <input type="file" id="pm-import-file" style="display: none;" accept=".json">
  75. `;
  76. document.body.appendChild(container);
  77.  
  78. // 主样式(完全保持原样)
  79. const style = document.createElement('style');
  80. style.textContent = `
  81. #prompt-manager {
  82. position: fixed;
  83. top: 20px;
  84. right: 20px;
  85. width: 320px;
  86. background: #fff;
  87. border-radius: 12px;
  88. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  89. z-index: 9999;
  90. font-family: 'Helvetica Neue', Arial, sans-serif;
  91. transition: all 0.3s ease;
  92. }
  93.  
  94. #prompt-manager.minimized {
  95. width: 40px;
  96. height: 40px;
  97. border-radius: 50%;
  98. overflow: hidden;
  99. cursor: pointer;
  100. }
  101.  
  102. #prompt-manager.minimized .pm-header {
  103. border-bottom: none;
  104. padding: 0;
  105. height: 100%;
  106. display: flex;
  107. align-items: center;
  108. justify-content: center;
  109. }
  110.  
  111. #prompt-manager.minimized .pm-header h3 {
  112. display: none;
  113. }
  114.  
  115. #prompt-manager.minimized .pm-header-buttons {
  116. display: none;
  117. }
  118.  
  119. #prompt-manager.minimized .pm-list,
  120. #prompt-manager.minimized .pm-modal,
  121. #prompt-manager.minimized .pm-search-container {
  122. display: none !important;
  123. }
  124.  
  125. #prompt-manager.minimized .pm-header::before {
  126. content: 'AI';
  127. color: #1890ff;
  128. font-size: 16px;
  129. font-weight: bold;
  130. }
  131.  
  132. .pm-header {
  133. display: flex;
  134. justify-content: space-between;
  135. align-items: center;
  136. padding: 16px;
  137. border-bottom: 1px solid #f0f0f0;
  138. }
  139.  
  140. .pm-header h3 {
  141. margin: 0;
  142. font-size: 16px;
  143. color: #1f1f1f;
  144. }
  145.  
  146. .pm-header-buttons {
  147. display: flex;
  148. gap: 8px;
  149. align-items: center; /* 垂直居中 */
  150. }
  151.  
  152. .pm-icon-btn {
  153. width: 32px;
  154. height: 32px;
  155. border: none;
  156. background: #1890ff;
  157. border-radius: 6px;
  158. display: flex;
  159. align-items: center;
  160. justify-content: center;
  161. cursor: pointer;
  162. transition: all 0.2s;
  163. color: #fff;
  164. }
  165.  
  166. .pm-icon-btn:hover {
  167. background: #40a9ff;
  168. }
  169.  
  170. .pm-icon-btn#pm-minimize {
  171. background: #blue;
  172. color: blue;
  173. border: 1px solid #1890ff;
  174. font-size: 14px;
  175. font-weight: bold;
  176. }
  177.  
  178. .pm-icon {
  179. color: #fff;
  180. font-size: 20px;
  181. line-height: 1;
  182. }
  183.  
  184. .pm-list {
  185. max-height: 150px; /* 固定高度为200px */
  186. overflow-y: auto;
  187. padding: 8px;
  188. }
  189.  
  190. .pm-item {
  191. display: flex;
  192. align-items: center;
  193. padding: 12px;
  194. margin: 4px;
  195. background: #f8fafb;
  196. border-radius: 8px;
  197. transition: background 0.2s;
  198. cursor: pointer;
  199. }
  200.  
  201. .pm-item:hover {
  202. background: #e6f4ff;
  203. }
  204.  
  205. .pm-item-title {
  206. flex: 1;
  207. font-size: 14px;
  208. color: #434343;
  209. overflow: hidden;
  210. text-overflow: ellipsis;
  211. }
  212.  
  213. .pm-item-actions {
  214. display: flex;
  215. gap: 8px;
  216. opacity: 0;
  217. transition: opacity 0.2s;
  218. }
  219.  
  220. .pm-item:hover .pm-item-actions {
  221. opacity: 1;
  222. }
  223.  
  224. .pm-modal {
  225. display: none;
  226. position: fixed;
  227. top: 0;
  228. left: 0;
  229. right: 0;
  230. bottom: 0;
  231. background: rgba(0, 0, 0, 0.4);
  232. justify-content: center;
  233. align-items: center;
  234. overflow: auto;
  235. }
  236.  
  237. .pm-modal-content {
  238. background: #fff;
  239. width: 440px;
  240. border-radius: 12px;
  241. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  242. max-height: 90vh;
  243. overflow: auto;
  244. }
  245.  
  246. .pm-modal-header {
  247. display: flex;
  248. justify-content: space-between;
  249. align-items: center;
  250. padding: 16px;
  251. border-bottom: 1px solid #f0f0f0;
  252. }
  253.  
  254. .pm-modal-header h4 {
  255. margin: 0;
  256. font-size: 16px;
  257. color: #1d1d1d;
  258. }
  259.  
  260. .pm-modal-body {
  261. padding: 16px;
  262. }
  263.  
  264. .pm-input, .pm-textarea {
  265. width: 90%;
  266. padding: 8px 12px;
  267. border: 1px solid #e0e0e0;
  268. border-radius: 6px;
  269. margin: 8px 0;
  270. font-size: 14px;
  271. transition: border-color 0.2s;
  272. }
  273.  
  274. .pm-input:focus, .pm-textarea:focus {
  275. border-color: #1890ff;
  276. outline: none;
  277. }
  278.  
  279. .pm-textarea {
  280. height: 100px;
  281. resize: vertical;
  282. }
  283.  
  284. .pm-modal-footer {
  285. padding: 16px;
  286. text-align: right;
  287. border-top: 1px solid #f0f0f0;
  288. }
  289.  
  290. .pm-btn {
  291. padding: 8px 16px;
  292. border: none;
  293. border-radius: 6px;
  294. cursor: pointer;
  295. font-size: 14px;
  296. transition: all 0.2s;
  297. }
  298.  
  299. .pm-primary {
  300. background: #1890ff;
  301. color: #fff;
  302. }
  303.  
  304. .pm-primary:hover {
  305. background: #40a9ff;
  306. }
  307.  
  308. .pm-btn:not(.pm-primary) {
  309. background: #f5f5f5;
  310. color: #666;
  311. margin-left: 8px;
  312. }
  313.  
  314. .pm-btn:not(.pm-primary):hover {
  315. background: #e0e0e0;
  316. }
  317.  
  318. .pm-search-container {
  319. padding: 8px;
  320. border-top: 1px solid #f0f0f0;
  321. }
  322. `;
  323. document.head.appendChild(style);
  324.  
  325. // 新增:焦点追踪逻辑
  326. document.addEventListener('focusin', (e) => {
  327. if (!container.contains(e.target)) {
  328. const target = e.target;
  329. if (target.matches('input, textarea, [contenteditable="true"]')) {
  330. lastFocusedElement = target;
  331. }
  332. }
  333. });
  334.  
  335. // 渲染列表
  336. function renderList() {
  337. const list = document.getElementById('pm-list');
  338. list.innerHTML = ''; // 清空列表
  339.  
  340. filteredPrompts.forEach((item, index) => {
  341. const listItem = document.createElement('div');
  342. listItem.classList.add('pm-item');
  343. listItem.innerHTML = `
  344. <span class="pm-item-title">${item.title}</span>
  345. <div class="pm-item-actions">
  346. <button class="pm-icon-btn" style="background:#52c41a">
  347. <span class="pm-icon">✎</span>
  348. </button>
  349. <button class="pm-icon-btn" style="background:#ff4d4f">
  350. <span class="pm-icon">✕</span>
  351. </button>
  352. </div>
  353. `;
  354.  
  355. // 修改事件处理
  356. const [editBtn, deleteBtn] = listItem.querySelectorAll('.pm-icon-btn');
  357.  
  358. // 阻止按钮获取焦点
  359. editBtn.addEventListener('mousedown', e => e.preventDefault());
  360. deleteBtn.addEventListener('mousedown', e => e.preventDefault());
  361.  
  362. editBtn.addEventListener('click', (e) => {
  363. e.stopPropagation();
  364. editPrompt(prompts.indexOf(item)); // 传递原始索引
  365. });
  366.  
  367. deleteBtn.addEventListener('click', (e) => {
  368. e.stopPropagation();
  369. deletePrompt(prompts.indexOf(item)); // 传递原始索引
  370. });
  371.  
  372. // 阻止列表项获取焦点
  373. listItem.addEventListener('mousedown', e => e.preventDefault());
  374.  
  375. listItem.addEventListener('click', () => {
  376. if (lastFocusedElement) {
  377. pasteToFocusedElement(item.content);
  378. } else {
  379. alert('请先点击需要输入的位置');
  380. }
  381. });
  382.  
  383. list.appendChild(listItem);
  384. });
  385. }
  386.  
  387. // 修改粘贴函数
  388. function pasteToFocusedElement(text) {
  389. if (!lastFocusedElement) return;
  390.  
  391. try {
  392. // 强制恢复焦点
  393. lastFocusedElement.focus();
  394.  
  395. if (lastFocusedElement.isContentEditable) {
  396. const selection = window.getSelection();
  397. const range = selection.getRangeAt(0);
  398. range.deleteContents();
  399. const textNode = document.createTextNode(text);
  400. range.insertNode(textNode);
  401. range.setStartAfter(textNode);
  402. selection.collapseToEnd();
  403. } else {
  404. const elem = lastFocusedElement;
  405. const start = elem.selectionStart;
  406. elem.setRangeText(text, start, start, 'end');
  407. elem.selectionStart = elem.selectionEnd = start + text.length;
  408. }
  409.  
  410. // 触发输入事件
  411. const event = new Event('input', { bubbles: true });
  412. lastFocusedElement.dispatchEvent(event);
  413. } catch (error) {
  414. console.error('粘贴失败,使用剪贴板回退');
  415. navigator.clipboard.writeText(text);
  416. alert('已复制到剪贴板,请手动粘贴');
  417. }
  418. }
  419.  
  420. // 编辑提示词
  421. window.editPrompt = function(index) {
  422. currentEditIndex = index;
  423. document.getElementById('pm-title').value = prompts[index].title;
  424. document.getElementById('pm-content').value = prompts[index].content;
  425. document.getElementById('pm-modal').style.display = 'flex';
  426. };
  427.  
  428. // 删除提示词
  429. window.deletePrompt = function(index) {
  430. prompts.splice(index, 1);
  431. localStorage.setItem(STORAGE_KEY, JSON.stringify(prompts));
  432. filterPrompts(); // 重新过滤列表
  433. };
  434.  
  435. // 添加新提示词
  436. document.getElementById('pm-add').addEventListener('click', () => {
  437. currentEditIndex = null;
  438. document.getElementById('pm-title').value = '';
  439. document.getElementById('pm-content').value = '';
  440. document.getElementById('pm-modal').style.display = 'flex';
  441. });
  442.  
  443. // 关闭模态框
  444. document.getElementById('pm-close').addEventListener('click', () => {
  445. document.getElementById('pm-modal').style.display = 'none';
  446. });
  447.  
  448. // 取消操作
  449. document.getElementById('pm-cancel').addEventListener('click', () => {
  450. document.getElementById('pm-modal').style.display = 'none';
  451. });
  452.  
  453. // 保存提示词
  454. document.getElementById('pm-save').addEventListener('click', () => {
  455. const title = document.getElementById('pm-title').value.trim();
  456. const content = document.getElementById('pm-content').value.trim();
  457.  
  458. if (!title || !content) {
  459. alert('标题和内容不能为空');
  460. return;
  461. }
  462.  
  463. if (currentEditIndex !== null) {
  464. prompts[currentEditIndex] = { title, content };
  465. } else {
  466. prompts.push({ title, content });
  467. }
  468.  
  469. localStorage.setItem(STORAGE_KEY, JSON.stringify(prompts));
  470. filterPrompts(); // 刷新列表
  471. document.getElementById('pm-modal').style.display = 'none';
  472. });
  473.  
  474. // 最小化功能
  475. document.getElementById('pm-minimize').addEventListener('click', (e) => {
  476. e.stopPropagation();
  477. isMinimized = !isMinimized;
  478. container.classList.toggle('minimized', isMinimized);
  479. });
  480.  
  481. container.addEventListener('click', () => {
  482. if (isMinimized) {
  483. isMinimized = false;
  484. container.classList.remove('minimized');
  485. }
  486. });
  487.  
  488. // 导出功能
  489. document.getElementById('pm-export').addEventListener('click', () => {
  490. const json = JSON.stringify(prompts);
  491. const blob = new Blob([json], { type: 'application/json' });
  492. const url = URL.createObjectURL(blob);
  493. const a = document.createElement('a');
  494. a.href = url;
  495. a.download = 'prompts.json';
  496. document.body.appendChild(a);
  497. a.click();
  498. document.body.removeChild(a);
  499. URL.revokeObjectURL(url);
  500. });
  501.  
  502. // 导入功能
  503. document.getElementById('pm-import').addEventListener('click', () => {
  504. document.getElementById('pm-import-file').click();
  505. });
  506.  
  507. document.getElementById('pm-import-file').addEventListener('change', (event) => {
  508. const file = event.target.files[0];
  509. if (file) {
  510. const reader = new FileReader();
  511. reader.onload = (e) => {
  512. try {
  513. const json = JSON.parse(e.target.result);
  514. if (Array.isArray(json)) {
  515. prompts = json;
  516. localStorage.setItem(STORAGE_KEY, JSON.stringify(prompts));
  517. filterPrompts(); // 重新渲染列表
  518. alert('导入成功');
  519. } else {
  520. alert('文件格式不正确,应为JSON数组');
  521. }
  522. } catch (error) {
  523. alert('文件解析失败:' + error);
  524. }
  525. };
  526. reader.readAsText(file);
  527. }
  528. });
  529.  
  530. // 搜索功能
  531. const searchInput = document.getElementById('pm-search');
  532. searchInput.addEventListener('input', () => {
  533. filterPrompts(searchInput.value.trim());
  534. });
  535.  
  536. // 过滤提示词
  537. function filterPrompts(searchTerm = '') {
  538. if (searchTerm) {
  539. const lowerSearchTerm = searchTerm.toLowerCase();
  540. filteredPrompts = prompts.filter(item =>
  541. item.title.toLowerCase().includes(lowerSearchTerm) ||
  542. item.content.toLowerCase().includes(lowerSearchTerm)
  543. );
  544. } else {
  545. filteredPrompts = [...prompts]; // 恢复到所有提示词
  546. }
  547. renderList();
  548. }
  549.  
  550. // 初始化
  551. filterPrompts();
  552. })();