GPT Prompt Manager: Deepseek and ChatGPT

Manage, store, and quickly insert GPT prompts with categorization, templating, tagging, rating, and usage tracking.

  1. // ==UserScript==
  2. // @name GPT Prompt Manager: Deepseek and ChatGPT
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.4
  5. // @author Minhaz Mahmood
  6. // @description Manage, store, and quickly insert GPT prompts with categorization, templating, tagging, rating, and usage tracking.
  7. // @match *://chatgpt.com/*
  8. // @match *://deepseek.com/*
  9. // @match *://chat.deepseek.com/*
  10. // @match *://chat.openai.com/*
  11. // @match *://*.chat.openai.com/*
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. /***********************************************************************
  21. * Configuration
  22. ***********************************************************************/
  23. const CONFIG = {
  24. MAX_ITEMS: 200,
  25. MAX_FAVORITES: 10,
  26. TOAST_DURATION: 2000,
  27. CONFIRM_TIMEOUT: 5000,
  28. STORAGE_KEY: 'gpt-prompts',
  29. KEYBOARD_SHORTCUT: { ctrlKey: true, altKey: true, key: 'p' },
  30. TABS: ['Coding', 'Writing', 'Research', 'General', 'Templates', 'Archive'],
  31. // Selector for ChatGPT input field; update if ChatGPT's UI changes.
  32. CHATGPT_INPUT_SELECTOR: 'textarea[data-id="root"]'
  33. };
  34.  
  35. /***********************************************************************
  36. * CSS Styles and UI Layout
  37. ***********************************************************************/
  38. const styles = `
  39. /* General transitions */
  40. .prompt-manager, .clip-toggle, .prompt-content, .prompt-bottom-actions,
  41. .prompt-header, .prompt-title, .prompt-close, .prompt-toast, .prompt-card,
  42. .prompt-preview, .prompt-actions, .prompt-btn, .prompt-search, .prompt-controls,
  43. .prompt-edit-icon, .prompt-save-icon, .prompt-drag-handle, .prompt-theme-toggle,
  44. .prompt-tabs, .prompt-tab, .prompt-tags, .prompt-import, .prompt-export {
  45. transition: all 0.2s ease;
  46. }
  47.  
  48. /* Manager container */
  49. .prompt-manager {
  50. position: fixed;
  51. left: 50%;
  52. top: 50%;
  53. transform: translate(-50%, -50%);
  54. width: 640px;
  55. height: 720px;
  56. background: #1a1b1e;
  57. border-radius: 16px;
  58. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  59. z-index: 99999;
  60. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  61. display: flex;
  62. flex-direction: column;
  63. opacity: 0;
  64. visibility: hidden;
  65. user-select: none;
  66. resize: both;
  67. overflow: hidden;
  68. }
  69. .prompt-manager.open {
  70. opacity: 1;
  71. visibility: visible;
  72. }
  73.  
  74. /* Floating Toggle Button (exact as original) */
  75. .clip-toggle {
  76. position: fixed;
  77. right: 340px;
  78. top: 10px;
  79. background: #2c2d31;
  80. border: none;
  81. border-radius: 12px;
  82. padding: 12px;
  83. cursor: pointer;
  84. z-index: 10001;
  85. display: flex;
  86. align-items: center;
  87. justify-content: center;
  88. }
  89. .clip-toggle.hidden {
  90. opacity: 0;
  91. visibility: hidden;
  92. pointer-events: none;
  93. }
  94. .clip-toggle:hover {
  95. transform: scale(1.05);
  96. background: #3a3b3f;
  97. }
  98.  
  99. /* Header and Tabs */
  100. .prompt-header {
  101. padding: 10px 20px;
  102. color: #fff;
  103. border-bottom: 1px solid #2c2d31;
  104. display: flex;
  105. justify-content: space-between;
  106. align-items: center;
  107. cursor: move;
  108. }
  109. .prompt-title {
  110. margin: 0;
  111. font-size: 18px;
  112. font-weight: 600;
  113. }
  114. .prompt-close {
  115. background: transparent;
  116. border: none;
  117. color: #6b7280;
  118. font-size: 24px;
  119. cursor: pointer;
  120. padding: 4px 8px;
  121. border-radius: 6px;
  122. }
  123. .prompt-close:hover {
  124. background: rgba(255, 255, 255, 0.1);
  125. color: #fff;
  126. }
  127. .prompt-tabs {
  128. display: flex;
  129. gap: 12px;
  130. padding: 0 20px;
  131. border-bottom: 1px solid #2c2d31;
  132. background: #24252a;
  133. }
  134. .prompt-tab {
  135. cursor: pointer;
  136. padding: 8px 12px;
  137. color: #ccc;
  138. border-bottom: 2px solid transparent;
  139. }
  140. .prompt-tab.active {
  141. color: #fff;
  142. border-color: #98c379;
  143. }
  144.  
  145. /* Controls: Search, Theme Toggle, Import/Export */
  146. .prompt-controls {
  147. margin: 10px 20px;
  148. display: flex;
  149. gap: 16px;
  150. align-items: center;
  151. flex-wrap: wrap;
  152. }
  153. .prompt-search {
  154. background: #2c2d31;
  155. color: #fff;
  156. border: 1px solid #3a3b3f;
  157. padding: 8px 12px;
  158. border-radius: 6px;
  159. flex: 1;
  160. }
  161. .prompt-theme-toggle, .prompt-import, .prompt-export {
  162. background: #5a67d8;
  163. color: #fff;
  164. padding: 8px 12px;
  165. border: none;
  166. border-radius: 6px;
  167. cursor: pointer;
  168. }
  169. .prompt-theme-toggle:hover, .prompt-import:hover, .prompt-export:hover {
  170. background: #6875f5;
  171. }
  172.  
  173. /* Content Area */
  174. .prompt-content {
  175. flex: 1;
  176. overflow-y: auto;
  177. padding: 10px 20px;
  178. margin-bottom: 60px;
  179. }
  180. .prompt-content::-webkit-scrollbar {
  181. width: 6px;
  182. }
  183. .prompt-content::-webkit-scrollbar-track {
  184. background: #1a1b1e;
  185. }
  186. .prompt-content::-webkit-scrollbar-thumb {
  187. background: #2c2d31;
  188. border-radius: 3px;
  189. }
  190. .prompt-content::-webkit-scrollbar-thumb:hover {
  191. background: #3a3b3f;
  192. }
  193.  
  194. /* Bottom Actions */
  195. .prompt-bottom-actions {
  196. display: flex;
  197. justify-content: space-between;
  198. align-items: center;
  199. padding: 16px 20px;
  200. background: #2c2d31;
  201. border-radius: 0 0 16px 16px;
  202. }
  203. .prompt-save, .prompt-save-clipboard, .prompt-clear-all {
  204. height: 40px;
  205. padding: 0 24px;
  206. border-radius: 8px;
  207. cursor: pointer;
  208. font-weight: 600;
  209. font-size: 14px;
  210. line-height: 40px;
  211. white-space: nowrap;
  212. border: none;
  213. }
  214. .prompt-save {
  215. background: #98c379;
  216. color: #1a1b1e;
  217. box-shadow: 0 2px 8px rgba(152, 195, 121, 0.2);
  218. }
  219. .prompt-save:hover {
  220. background: #a9d389;
  221. transform: translateY(-2px);
  222. box-shadow: 0 4px 12px rgba(152, 195, 121, 0.3);
  223. }
  224. .prompt-save:disabled {
  225. background: #4a4b4f;
  226. cursor: not-allowed;
  227. }
  228. .prompt-save-clipboard {
  229. background: #5a67d8;
  230. color: #fff;
  231. box-shadow: 0 2px 8px rgba(90, 103, 216, 0.2);
  232. }
  233. .prompt-save-clipboard:hover {
  234. background: #6875f5;
  235. }
  236. .prompt-clear-all {
  237. background: #dc2626;
  238. color: #fff;
  239. box-shadow: 0 2px 8px rgba(220, 38, 38, 0.2);
  240. padding-right: 20px;
  241. text-align: center;
  242. }
  243. .prompt-clear-all:hover {
  244. background: #ef4444;
  245. }
  246. .prompt-clear-all.confirm {
  247. background: #991b1b;
  248. }
  249.  
  250. /* Prompt Card Styles */
  251. .prompt-card {
  252. background: #2c2d31;
  253. border-radius: 12px;
  254. padding: 12px;
  255. margin-bottom: 12px;
  256. color: #fff;
  257. border: 1px solid #3a3b3f;
  258. }
  259. .prompt-card:hover {
  260. transform: translateY(-2px);
  261. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  262. border-color: #4a4b4f;
  263. }
  264. .prompt-title-wrapper {
  265. display: flex;
  266. align-items: center;
  267. gap: 8px;
  268. margin-bottom: 4px;
  269. }
  270. .prompt-title-display {
  271. font-size: 16px;
  272. color: #fff;
  273. overflow: hidden;
  274. text-overflow: ellipsis;
  275. white-space: nowrap;
  276. flex: 1;
  277. }
  278. .prompt-edit-icon, .prompt-save-icon {
  279. background: transparent;
  280. border: none;
  281. color: #6b7280;
  282. font-size: 16px;
  283. cursor: pointer;
  284. padding: 4px 8px;
  285. border-radius: 6px;
  286. }
  287. .prompt-edit-icon:hover, .prompt-save-icon:hover {
  288. background: rgba(255, 255, 255, 0.1);
  289. color: #fff;
  290. }
  291. .prompt-preview {
  292. font-size: 14px;
  293. color: #d1d5db;
  294. margin: 4px 0 8px 0;
  295. line-height: 1.5;
  296. max-height: 90px;
  297. overflow-y: auto;
  298. white-space: pre-wrap;
  299. word-break: break-word;
  300. }
  301. .prompt-info {
  302. font-size: 12px;
  303. color: #9ca3af;
  304. margin-bottom: 4px;
  305. }
  306. .prompt-tags {
  307. margin-top: 4px;
  308. font-size: 12px;
  309. }
  310. .prompt-tags span {
  311. color: #98c379;
  312. cursor: pointer;
  313. margin-right: 6px;
  314. }
  315.  
  316. /* Action buttons within each prompt card */
  317. .prompt-actions {
  318. display: flex;
  319. gap: 8px;
  320. justify-content: flex-end;
  321. border-top: 1px solid #3a3b3f;
  322. padding-top: 8px;
  323. margin-top: 4px;
  324. }
  325. .prompt-btn {
  326. height: 32px;
  327. padding: 0 12px;
  328. background: transparent;
  329. border: 1px solid #4a4b4f;
  330. color: #fff;
  331. border-radius: 6px;
  332. font-size: 13px;
  333. line-height: 30px;
  334. }
  335. .prompt-btn:hover {
  336. background: #3a3b3f;
  337. border-color: #5a5b5f;
  338. }
  339. .prompt-btn.delete {
  340. color: #ef4444;
  341. border-color: #ef4444;
  342. }
  343. .prompt-btn.delete:hover {
  344. background: rgba(239, 68, 68, 0.1);
  345. }
  346.  
  347. /* Rating stars */
  348. .prompt-rating {
  349. display: flex;
  350. gap: 4px;
  351. align-items: center;
  352. margin-right: auto;
  353. }
  354. .prompt-rating-star {
  355. cursor: pointer;
  356. color: #ccc;
  357. }
  358. .prompt-rating-star.filled {
  359. color: #ffc107;
  360. }
  361.  
  362. /* Resize drag handle */
  363. .prompt-drag-handle {
  364. position: absolute;
  365. right: 2px;
  366. bottom: 2px;
  367. width: 16px;
  368. height: 16px;
  369. cursor: nwse-resize;
  370. background: rgba(255, 255, 255, 0.3);
  371. border-radius: 3px;
  372. }
  373.  
  374. /* Toast notification styling */
  375. .prompt-toast {
  376. position: fixed;
  377. bottom: 20px;
  378. left: 50%;
  379. transform: translateX(-50%);
  380. background: #333;
  381. color: #fff;
  382. padding: 10px 16px;
  383. border-radius: 8px;
  384. z-index: 999999;
  385. }
  386.  
  387. /* Light theme overrides */
  388. .prompt-manager[data-theme="light"] {
  389. background: #ffffff;
  390. color: #1a1b1e;
  391. }
  392. .prompt-manager[data-theme="light"] .prompt-search {
  393. background: #f0f0f0;
  394. color: #1a1b1e;
  395. }
  396. .prompt-manager[data-theme="light"] .prompt-card {
  397. background: #f8f8f8;
  398. color: #1a1b1e;
  399. }
  400. `;
  401.  
  402. // Append the stylesheet to the document head.
  403. const styleSheet = document.createElement('style');
  404. styleSheet.textContent = styles;
  405. document.head.appendChild(styleSheet);
  406.  
  407. /***********************************************************************
  408. * UI Elements: Manager Window & Floating Toggle Button
  409. ***********************************************************************/
  410. const manager = document.createElement('div');
  411. manager.className = 'prompt-manager';
  412. manager.dataset.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  413.  
  414. // Manager HTML layout with header, tabs, controls, content, and bottom actions.
  415. manager.innerHTML = `
  416. <div class="prompt-header">
  417. <h2 class="prompt-title">GPT Prompt Manager</h2>
  418. <button class="prompt-close">×</button>
  419. </div>
  420. <div class="prompt-tabs">
  421. ${CONFIG.TABS.map(tab => `<div class="prompt-tab" data-tab="${tab}">${tab}</div>`).join('')}
  422. </div>
  423. <div class="prompt-controls">
  424. <input type="text" class="prompt-search" placeholder="Search prompts by title, content, or tag...">
  425. <button class="prompt-theme-toggle">Toggle Theme</button>
  426. <button class="prompt-import">Import</button>
  427. <button class="prompt-export">Export</button>
  428. </div>
  429. <div class="prompt-content"></div>
  430. <div class="prompt-bottom-actions">
  431. <button class="prompt-save-clipboard">Save Clipboard</button>
  432. <button class="prompt-save" disabled>Save Selection</button>
  433. <button class="prompt-clear-all">Clear All</button>
  434. </div>
  435. <div class="prompt-drag-handle"></div>
  436. `;
  437.  
  438. // Floating toggle button using the exact SVG and styling from the original script.
  439. const toggleBtn = document.createElement('button');
  440. toggleBtn.className = 'clip-toggle';
  441. toggleBtn.innerHTML = `
  442. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
  443. <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
  444. <rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
  445. </svg>
  446. `;
  447.  
  448. /***********************************************************************
  449. * GPT Prompt Manager Class Definition
  450. ***********************************************************************/
  451. class GptPromptManager {
  452. constructor() {
  453. this.prompts = this.loadPrompts();
  454. this.archivedPrompts = this.loadArchivedPrompts();
  455. this.isOpen = false;
  456. this.clearAllTimeout = null;
  457. this.searchTerm = '';
  458. this.activeTab = 'General';
  459. this.manualTheme = null; // null indicates following system preference
  460. this.activeTagFilter = '';
  461.  
  462. // Append UI elements
  463. document.body.appendChild(manager);
  464. document.body.appendChild(toggleBtn);
  465.  
  466. // Initialize events, theme detection, drag/resize, and initial render.
  467. this.initEvents();
  468. this.setupThemeDetection();
  469. this.makeDraggable();
  470. this.renderTabs();
  471. this.renderPrompts();
  472. this.updateSaveButton();
  473. }
  474.  
  475. /***********************************************************************
  476. * Data Persistence: Loading & Saving Prompts
  477. ***********************************************************************/
  478. loadPrompts() {
  479. try {
  480. const saved = typeof GM_getValue !== 'undefined'
  481. ? GM_getValue(CONFIG.STORAGE_KEY)
  482. : localStorage.getItem(CONFIG.STORAGE_KEY);
  483. return saved ? JSON.parse(saved) : [];
  484. } catch (err) {
  485. console.error('Error loading prompts:', err);
  486. return [];
  487. }
  488. }
  489.  
  490. loadArchivedPrompts() {
  491. try {
  492. const archived = typeof GM_getValue !== 'undefined'
  493. ? GM_getValue(CONFIG.STORAGE_KEY + '_archived')
  494. : localStorage.getItem(CONFIG.STORAGE_KEY + '_archived');
  495. return archived ? JSON.parse(archived) : [];
  496. } catch (err) {
  497. console.error('Error loading archived prompts:', err);
  498. return [];
  499. }
  500. }
  501.  
  502. savePrompts() {
  503. try {
  504. const data = JSON.stringify(this.prompts);
  505. if (typeof GM_setValue !== 'undefined') {
  506. GM_setValue(CONFIG.STORAGE_KEY, data);
  507. } else {
  508. localStorage.setItem(CONFIG.STORAGE_KEY, data);
  509. }
  510. } catch (err) {
  511. console.error('Error saving prompts:', err);
  512. this.showToast('Error saving prompts');
  513. }
  514. }
  515.  
  516. saveArchivedPrompts() {
  517. try {
  518. const data = JSON.stringify(this.archivedPrompts);
  519. if (typeof GM_setValue !== 'undefined') {
  520. GM_setValue(CONFIG.STORAGE_KEY + '_archived', data);
  521. } else {
  522. localStorage.setItem(CONFIG.STORAGE_KEY + '_archived', data);
  523. }
  524. } catch (err) {
  525. console.error('Error saving archived prompts:', err);
  526. }
  527. }
  528.  
  529. /***********************************************************************
  530. * Theme Detection and Toggling
  531. ***********************************************************************/
  532. setupThemeDetection() {
  533. const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  534. mediaQuery.addEventListener('change', e => {
  535. if (this.manualTheme === null) {
  536. manager.dataset.theme = e.matches ? 'dark' : 'light';
  537. }
  538. });
  539. }
  540.  
  541. /***********************************************************************
  542. * Event Binding
  543. ***********************************************************************/
  544. initEvents() {
  545. // Toggle manager visibility
  546. toggleBtn.addEventListener('click', () => this.toggle());
  547. manager.querySelector('.prompt-close').addEventListener('click', () => this.close());
  548.  
  549. // Save selected text as a new prompt
  550. document.addEventListener('selectionchange', () => this.updateSaveButton());
  551. manager.querySelector('.prompt-save').addEventListener('click', () => {
  552. const selection = window.getSelection().toString().trim();
  553. if (selection) {
  554. this.addPrompt(selection);
  555. }
  556. });
  557.  
  558. // Save clipboard text as a new prompt
  559. manager.querySelector('.prompt-save-clipboard').addEventListener('click', async () => {
  560. try {
  561. const clipboardText = await navigator.clipboard.readText();
  562. if (clipboardText.trim()) {
  563. this.addPrompt(clipboardText);
  564. } else {
  565. this.showToast('Clipboard is empty');
  566. }
  567. } catch (err) {
  568. console.error('Clipboard error:', err);
  569. this.showToast('Failed to read clipboard');
  570. }
  571. });
  572.  
  573. // Clear all prompts (with confirmation)
  574. manager.querySelector('.prompt-clear-all').addEventListener('click', e => {
  575. this.handleClearAll(e.target);
  576. });
  577.  
  578. // Tab switching
  579. manager.querySelectorAll('.prompt-tab').forEach(tab => {
  580. tab.addEventListener('click', () => {
  581. this.activeTab = tab.dataset.tab;
  582. this.activeTagFilter = '';
  583. this.renderTabs();
  584. this.renderPrompts();
  585. });
  586. });
  587.  
  588. // Search functionality
  589. manager.querySelector('.prompt-search').addEventListener('input', e => {
  590. this.searchTerm = e.target.value.toLowerCase();
  591. this.activeTagFilter = '';
  592. this.renderPrompts();
  593. });
  594.  
  595. // Theme toggle
  596. manager.querySelector('.prompt-theme-toggle').addEventListener('click', () => {
  597. if (this.manualTheme === 'dark') {
  598. this.manualTheme = 'light';
  599. } else if (this.manualTheme === 'light') {
  600. this.manualTheme = 'dark';
  601. } else {
  602. this.manualTheme = (manager.dataset.theme === 'dark') ? 'light' : 'dark';
  603. }
  604. manager.dataset.theme = this.manualTheme;
  605. });
  606.  
  607. // Import and Export prompts
  608. manager.querySelector('.prompt-import').addEventListener('click', () => this.importPrompts());
  609. manager.querySelector('.prompt-export').addEventListener('click', () => this.exportPrompts());
  610.  
  611. // Global keyboard shortcut: Ctrl+Alt+P
  612. document.addEventListener('keydown', e => {
  613. if (e.ctrlKey === CONFIG.KEYBOARD_SHORTCUT.ctrlKey &&
  614. e.altKey === CONFIG.KEYBOARD_SHORTCUT.altKey &&
  615. e.key.toLowerCase() === CONFIG.KEYBOARD_SHORTCUT.key) {
  616. e.preventDefault();
  617. this.toggle();
  618. const selection = window.getSelection().toString().trim();
  619. if (selection) {
  620. this.addPrompt(selection);
  621. }
  622. }
  623. if (e.key === 'Escape' && this.isOpen) {
  624. this.close();
  625. }
  626. });
  627.  
  628. // Delegate click events for prompt card actions
  629. manager.querySelector('.prompt-content').addEventListener('click', e => {
  630. const card = e.target.closest('.prompt-card');
  631. if (!card) return;
  632. const id = parseInt(card.dataset.id);
  633.  
  634. if (e.target.classList.contains('prompt-edit-icon') ||
  635. e.target.classList.contains('prompt-save-icon')) {
  636. this.toggleEditTitle(id, card);
  637. } else if (e.target.classList.contains('delete')) {
  638. if (this.activeTab === 'Archive') {
  639. this.deleteArchivedPrompt(id);
  640. } else {
  641. this.removePrompt(id);
  642. }
  643. } else if (e.target.classList.contains('favorite')) {
  644. this.toggleFavorite(id);
  645. } else if (e.target.classList.contains('move-up')) {
  646. e.stopPropagation();
  647. this.movePrompt(id, -1);
  648. } else if (e.target.classList.contains('move-down')) {
  649. e.stopPropagation();
  650. this.movePrompt(id, 1);
  651. } else if (e.target.classList.contains('copy')) {
  652. const text = card.querySelector('.prompt-preview').textContent;
  653. this.copyText(text);
  654. } else if (e.target.classList.contains('insert')) {
  655. const text = card.querySelector('.prompt-preview').textContent;
  656. this.insertIntoChatGpt(text);
  657. this.incrementUsage(id);
  658. } else if (e.target.classList.contains('prompt-rating-star')) {
  659. const starValue = parseInt(e.target.dataset.value);
  660. this.setRating(id, starValue);
  661. } else if (e.target.tagName === 'SPAN' && e.target.parentElement.classList.contains('prompt-tags')) {
  662. // Filter by tag when clicked
  663. this.activeTagFilter = e.target.textContent.slice(1).toLowerCase();
  664. manager.querySelector('.prompt-search').value = '';
  665. this.searchTerm = '';
  666. this.renderPrompts();
  667. }
  668. });
  669. }
  670.  
  671. /***********************************************************************
  672. * Draggable & Resizable Manager
  673. ***********************************************************************/
  674. makeDraggable() {
  675. const header = manager.querySelector('.prompt-header');
  676. header.addEventListener('mousedown', e => {
  677. e.preventDefault();
  678. const offsetX = e.clientX - manager.offsetLeft;
  679. const offsetY = e.clientY - manager.offsetTop;
  680. const onMouseMove = ev => {
  681. manager.style.left = (ev.clientX - offsetX) + 'px';
  682. manager.style.top = (ev.clientY - offsetY) + 'px';
  683. };
  684. const onMouseUp = () => document.removeEventListener('mousemove', onMouseMove);
  685. document.addEventListener('mousemove', onMouseMove);
  686. document.addEventListener('mouseup', onMouseUp, { once: true });
  687. });
  688.  
  689. // Enable resizing using the drag handle
  690. const dragHandle = manager.querySelector('.prompt-drag-handle');
  691. let isResizing = false, startX, startY, initialWidth, initialHeight;
  692. dragHandle.addEventListener('mousedown', e => {
  693. e.preventDefault();
  694. isResizing = true;
  695. startX = e.clientX;
  696. startY = e.clientY;
  697. initialWidth = manager.offsetWidth;
  698. initialHeight = manager.offsetHeight;
  699.  
  700. const onMouseMove = ev => {
  701. if (isResizing) {
  702. const newWidth = initialWidth + (ev.clientX - startX);
  703. const newHeight = initialHeight + (ev.clientY - startY);
  704. manager.style.width = newWidth + 'px';
  705. manager.style.height = newHeight + 'px';
  706. }
  707. };
  708. const onMouseUp = () => {
  709. isResizing = false;
  710. document.removeEventListener('mousemove', onMouseMove);
  711. };
  712. document.addEventListener('mousemove', onMouseMove);
  713. document.addEventListener('mouseup', onMouseUp, { once: true });
  714. });
  715. }
  716.  
  717. /***********************************************************************
  718. * Prompt Operations: Add, Remove, and Modify Prompts
  719. ***********************************************************************/
  720. addPrompt(text) {
  721. const timestamp = Date.now();
  722. const category = this.activeTab !== 'Archive' ? this.activeTab : 'General';
  723. const promptItem = {
  724. id: timestamp,
  725. title: `Prompt-${timestamp}`,
  726. text: text.trim(),
  727. date: new Date().toISOString(),
  728. url: window.location.href,
  729. isFavorite: false,
  730. category,
  731. tags: [],
  732. usageCount: 0,
  733. rating: 0
  734. };
  735.  
  736. // Insert at the beginning of non-favorite items.
  737. const index = this.prompts.findIndex(p => !p.isFavorite);
  738. this.prompts.splice(index === -1 ? this.prompts.length : index, 0, promptItem);
  739.  
  740. // Enforce maximum prompt limit and archive older items if needed.
  741. if (this.prompts.length > CONFIG.MAX_ITEMS) {
  742. const nonFavorites = this.prompts.filter(p => !p.isFavorite);
  743. if (nonFavorites.length) {
  744. const itemToArchive = nonFavorites[nonFavorites.length - 1];
  745. this.archivedPrompts.unshift(itemToArchive);
  746. this.prompts = this.prompts.filter(p => p.id !== itemToArchive.id);
  747. this.saveArchivedPrompts();
  748. }
  749. }
  750.  
  751. this.savePrompts();
  752. this.renderPrompts();
  753. this.showToast('Prompt saved');
  754. }
  755.  
  756. removePrompt(id) {
  757. this.prompts = this.prompts.filter(p => p.id !== id);
  758. // Attempt to restore one archived prompt if available.
  759. if (this.archivedPrompts.length && this.prompts.length < CONFIG.MAX_ITEMS) {
  760. const restoreItem = this.archivedPrompts.find(p => !p.isFavorite);
  761. if (restoreItem) {
  762. this.prompts.push(restoreItem);
  763. this.archivedPrompts = this.archivedPrompts.filter(p => p.id !== restoreItem.id);
  764. this.saveArchivedPrompts();
  765. this.showToast('Restored a prompt from archive');
  766. }
  767. }
  768. this.savePrompts();
  769. this.renderPrompts();
  770. this.showToast('Prompt deleted');
  771. }
  772.  
  773. deleteArchivedPrompt(id) {
  774. this.archivedPrompts = this.archivedPrompts.filter(p => p.id !== id);
  775. this.saveArchivedPrompts();
  776. this.renderPrompts();
  777. this.showToast('Archived prompt permanently deleted');
  778. }
  779.  
  780. movePrompt(id, direction) {
  781. const idx = this.prompts.findIndex(p => p.id === id);
  782. if (idx === -1) return;
  783. const newIdx = idx + direction;
  784. if (newIdx < 0 || newIdx >= this.prompts.length) {
  785. this.showToast('Cannot move prompt further');
  786. return;
  787. }
  788. const item = this.prompts[idx];
  789. const target = this.prompts[newIdx];
  790. // Prevent moving across favorite boundaries.
  791. if ((item.isFavorite && !target.isFavorite) || (!item.isFavorite && target.isFavorite)) {
  792. this.showToast('Cannot move across favorite sections');
  793. return;
  794. }
  795. this.prompts.splice(idx, 1);
  796. this.prompts.splice(newIdx, 0, item);
  797. this.savePrompts();
  798. this.renderPrompts();
  799. }
  800.  
  801. toggleFavorite(id) {
  802. const item = this.prompts.find(p => p.id === id);
  803. if (!item) return;
  804. const favoriteCount = this.prompts.filter(p => p.isFavorite).length;
  805. if (!item.isFavorite && favoriteCount >= CONFIG.MAX_FAVORITES) {
  806. this.showToast(`Maximum ${CONFIG.MAX_FAVORITES} favorites allowed`);
  807. return;
  808. }
  809. const idx = this.prompts.indexOf(item);
  810. this.prompts.splice(idx, 1);
  811. item.isFavorite = !item.isFavorite;
  812. if (item.isFavorite) {
  813. const lastFav = this.prompts.findLastIndex(p => p.isFavorite);
  814. this.prompts.splice(lastFav + 1, 0, item);
  815. } else {
  816. const firstNonFav = this.prompts.findIndex(p => !p.isFavorite);
  817. this.prompts.splice(firstNonFav === -1 ? this.prompts.length : firstNonFav, 0, item);
  818. }
  819. this.savePrompts();
  820. this.renderPrompts();
  821. this.showToast(item.isFavorite ? 'Marked as favorite' : 'Removed favorite');
  822. }
  823.  
  824. copyText(text) {
  825. navigator.clipboard.writeText(text)
  826. .then(() => this.showToast('Copied to clipboard'))
  827. .catch(err => {
  828. console.error('Copy error:', err);
  829. this.showToast('Failed to copy text');
  830. });
  831. }
  832.  
  833. insertIntoChatGpt(text) {
  834. const inputEl = document.querySelector(CONFIG.CHATGPT_INPUT_SELECTOR);
  835. if (inputEl) {
  836. inputEl.value = text;
  837. inputEl.dispatchEvent(new Event('input', { bubbles: true }));
  838. this.showToast('Prompt inserted');
  839. } else {
  840. this.showToast('ChatGPT input field not found');
  841. }
  842. }
  843.  
  844. incrementUsage(id) {
  845. const item = this.prompts.find(p => p.id === id);
  846. if (!item) return;
  847. item.usageCount = (item.usageCount || 0) + 1;
  848. this.savePrompts();
  849. this.renderPrompts();
  850. }
  851.  
  852. setRating(id, value) {
  853. const item = this.prompts.find(p => p.id === id);
  854. if (!item) return;
  855. item.rating = value;
  856. this.savePrompts();
  857. this.renderPrompts();
  858. }
  859.  
  860. /***********************************************************************
  861. * Edit Title and Tag Management
  862. ***********************************************************************/
  863. toggleEditTitle(id, card) {
  864. const titleEl = card.querySelector('.prompt-title-display');
  865. const editIcon = card.querySelector('.prompt-edit-icon');
  866. const saveIcon = card.querySelector('.prompt-save-icon');
  867. const item = this.getItemById(id);
  868. if (!item) return;
  869.  
  870. if (titleEl.contentEditable !== 'true') {
  871. // Enable editing; show title and tags
  872. card.classList.add('editing');
  873. titleEl.contentEditable = 'true';
  874. titleEl.textContent = item.title + (item.tags.length ? `, ${item.tags.join(', ')}` : '');
  875. titleEl.focus();
  876. // Place the cursor at the end
  877. const range = document.createRange();
  878. range.selectNodeContents(titleEl);
  879. range.collapse(false);
  880. const sel = window.getSelection();
  881. sel.removeAllRanges();
  882. sel.addRange(range);
  883. editIcon.style.display = 'none';
  884. saveIcon.style.display = 'inline-block';
  885. } else {
  886. // Save changes to title and tags
  887. const newText = titleEl.textContent.trim();
  888. if (!newText) {
  889. this.showToast('Title cannot be blank');
  890. return;
  891. }
  892. card.classList.remove('editing');
  893. titleEl.contentEditable = 'false';
  894. editIcon.style.display = 'inline-block';
  895. saveIcon.style.display = 'none';
  896. const [newTitle, ...tagParts] = newText.split(',');
  897. item.title = newTitle.trim();
  898. item.tags = tagParts.map(t => t.trim()).filter(t => t !== '');
  899. this.savePrompts();
  900. this.renderPrompts();
  901. }
  902. }
  903.  
  904. getItemById(id) {
  905. return this.prompts.find(p => p.id === id) ||
  906. this.archivedPrompts.find(p => p.id === id);
  907. }
  908.  
  909. /***********************************************************************
  910. * Clear All Prompts with Confirmation
  911. ***********************************************************************/
  912. handleClearAll(button) {
  913. if (button.classList.contains('confirm')) {
  914. this.prompts = [];
  915. this.archivedPrompts = [];
  916. this.savePrompts();
  917. this.saveArchivedPrompts();
  918. this.renderPrompts();
  919. this.showToast('All prompts cleared');
  920. button.textContent = 'Clear All';
  921. button.classList.remove('confirm');
  922. if (this.clearAllTimeout) {
  923. clearTimeout(this.clearAllTimeout);
  924. this.clearAllTimeout = null;
  925. }
  926. } else {
  927. button.textContent = 'Confirm Clear?';
  928. button.classList.add('confirm');
  929. if (this.clearAllTimeout) clearTimeout(this.clearAllTimeout);
  930. this.clearAllTimeout = setTimeout(() => {
  931. button.textContent = 'Clear All';
  932. button.classList.remove('confirm');
  933. this.clearAllTimeout = null;
  934. }, CONFIG.CONFIRM_TIMEOUT);
  935. }
  936. }
  937.  
  938. /***********************************************************************
  939. * Render Tabs and Prompt List
  940. ***********************************************************************/
  941. renderTabs() {
  942. manager.querySelectorAll('.prompt-tab').forEach(tabEl => {
  943. tabEl.classList.toggle('active', tabEl.dataset.tab === this.activeTab);
  944. });
  945. }
  946.  
  947. renderPrompts() {
  948. const content = manager.querySelector('.prompt-content');
  949. content.innerHTML = '';
  950.  
  951. let itemsToRender = (this.activeTab === 'Archive')
  952. ? this.archivedPrompts
  953. : this.prompts.filter(p => p.category === this.activeTab);
  954.  
  955. // Filter by search term and active tag
  956. itemsToRender = itemsToRender.filter(item => {
  957. const searchMatch = item.title.toLowerCase().includes(this.searchTerm) ||
  958. item.text.toLowerCase().includes(this.searchTerm);
  959. const tagMatch = this.activeTagFilter
  960. ? item.tags.map(t => t.toLowerCase()).includes(this.activeTagFilter)
  961. : true;
  962. return searchMatch && tagMatch;
  963. });
  964.  
  965. // Sort: favorites first, then by rating and usage
  966. itemsToRender.sort((a, b) => {
  967. if (b.isFavorite !== a.isFavorite) return b.isFavorite ? 1 : -1;
  968. if (b.rating !== a.rating) return b.rating - a.rating;
  969. return b.usageCount - a.usageCount;
  970. });
  971.  
  972. if (!itemsToRender.length) {
  973. content.innerHTML = `<div class="prompt-empty">
  974. ${(this.searchTerm || this.activeTagFilter) ? 'No matching prompts found' : 'No prompts saved yet'}
  975. </div>`;
  976. return;
  977. }
  978.  
  979. // Build HTML for each prompt card.
  980. content.innerHTML = itemsToRender.map(item => {
  981. const stars = [1,2,3,4,5].map(value => {
  982. const filled = value <= item.rating ? 'filled' : '';
  983. return `<span class="prompt-rating-star ${filled}" data-value="${value}">★</span>`;
  984. }).join('');
  985.  
  986. return `
  987. <div class="prompt-card ${item.isFavorite ? 'favorite' : ''}" data-id="${item.id}">
  988. <div class="prompt-title-wrapper">
  989. <button class="prompt-edit-icon">✎</button>
  990. <div class="prompt-title-display">${item.title}</div>
  991. <button class="prompt-save-icon" style="display:none;">💾</button>
  992. </div>
  993. <div class="prompt-preview">${item.text}</div>
  994. <div class="prompt-info">
  995. <span class="prompt-date">${new Date(item.date).toLocaleDateString()}</span>
  996. <br>
  997. <span class="prompt-url">[<a href="${item.url}" target="_blank">source</a>]</span>
  998. </div>
  999. <div class="prompt-tags">
  1000. ${item.tags.map(tag => `<span>#${tag}</span>`).join('')}
  1001. </div>
  1002. <div class="prompt-actions">
  1003. <div class="prompt-rating">${stars}</div>
  1004. <button class="prompt-btn favorite">${item.isFavorite ? '★' : '☆'}</button>
  1005. <button class="prompt-btn copy">Copy</button>
  1006. <button class="prompt-btn insert">Insert</button>
  1007. <button class="prompt-btn delete">Delete</button>
  1008. ${this.activeTab === 'Archive'
  1009. ? ''
  1010. : `<button class="prompt-btn move-up">↑</button>
  1011. <button class="prompt-btn move-down">↓</button>`
  1012. }
  1013. </div>
  1014. </div>
  1015. `;
  1016. }).join('');
  1017. }
  1018.  
  1019. /***********************************************************************
  1020. * Update Save Button Based on Selection
  1021. ***********************************************************************/
  1022. updateSaveButton() {
  1023. const saveBtn = manager.querySelector('.prompt-save');
  1024. if (!saveBtn) return;
  1025. const selection = window.getSelection();
  1026. saveBtn.disabled = !(selection && selection.toString().trim().length > 0);
  1027. }
  1028.  
  1029. /***********************************************************************
  1030. * Open/Close Manager & Toast Notifications
  1031. ***********************************************************************/
  1032. toggle() {
  1033. this.isOpen = !this.isOpen;
  1034. manager.classList.toggle('open');
  1035. // Do not hide the toggle icon once visible
  1036. // (Remove 'hidden' class if present)
  1037. toggleBtn.classList.remove('hidden');
  1038. }
  1039.  
  1040. close() {
  1041. this.isOpen = false;
  1042. manager.classList.remove('open');
  1043. // Keep the toggle icon visible
  1044. toggleBtn.classList.remove('hidden');
  1045. }
  1046.  
  1047. showToast(message) {
  1048. const existing = document.querySelector('.prompt-toast');
  1049. if (existing) existing.remove();
  1050. const toast = document.createElement('div');
  1051. toast.className = 'prompt-toast';
  1052. toast.textContent = message;
  1053. document.body.appendChild(toast);
  1054. setTimeout(() => toast.remove(), CONFIG.TOAST_DURATION);
  1055. }
  1056.  
  1057. /***********************************************************************
  1058. * Import/Export Functionality
  1059. ***********************************************************************/
  1060. importPrompts() {
  1061. const fileInput = document.createElement('input');
  1062. fileInput.type = 'file';
  1063. fileInput.accept = 'application/json';
  1064. fileInput.addEventListener('change', e => {
  1065. const file = e.target.files[0];
  1066. if (!file) return;
  1067. const reader = new FileReader();
  1068. reader.onload = evt => {
  1069. try {
  1070. const data = JSON.parse(evt.target.result);
  1071. if (Array.isArray(data.prompts)) {
  1072. data.prompts.forEach(item => this.prompts.push(item));
  1073. }
  1074. if (Array.isArray(data.archivedPrompts)) {
  1075. data.archivedPrompts.forEach(item => this.archivedPrompts.push(item));
  1076. }
  1077. this.savePrompts();
  1078. this.saveArchivedPrompts();
  1079. this.renderPrompts();
  1080. this.showToast('Prompts imported successfully');
  1081. } catch (err) {
  1082. console.error('Import error:', err);
  1083. this.showToast('Failed to import file');
  1084. }
  1085. };
  1086. reader.readAsText(file);
  1087. });
  1088. fileInput.click();
  1089. }
  1090.  
  1091. exportPrompts() {
  1092. const data = {
  1093. prompts: this.prompts,
  1094. archivedPrompts: this.archivedPrompts
  1095. };
  1096. const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  1097. const url = URL.createObjectURL(blob);
  1098. const a = document.createElement('a');
  1099. a.href = url;
  1100. a.download = 'gpt-prompts-export.json';
  1101. document.body.appendChild(a);
  1102. a.click();
  1103. setTimeout(() => {
  1104. document.body.removeChild(a);
  1105. URL.revokeObjectURL(url);
  1106. }, 0);
  1107. }
  1108. }
  1109.  
  1110. // Instantiate the GPT Prompt Manager.
  1111. new GptPromptManager();
  1112.  
  1113. })();