// ==UserScript==
// @name GPT Prompt Manager: Deepseek and ChatGPT
// @namespace http://tampermonkey.net/
// @version 1.0.4
// @author Minhaz Mahmood
// @description Manage, store, and quickly insert GPT prompts with categorization, templating, tagging, rating, and usage tracking.
// @match *://chatgpt.com/*
// @match *://deepseek.com/*
// @match *://chat.deepseek.com/*
// @match *://chat.openai.com/*
// @match *://*.chat.openai.com/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
/***********************************************************************
* Configuration
***********************************************************************/
const CONFIG = {
MAX_ITEMS: 200,
MAX_FAVORITES: 10,
TOAST_DURATION: 2000,
CONFIRM_TIMEOUT: 5000,
STORAGE_KEY: 'gpt-prompts',
KEYBOARD_SHORTCUT: { ctrlKey: true, altKey: true, key: 'p' },
TABS: ['Coding', 'Writing', 'Research', 'General', 'Templates', 'Archive'],
// Selector for ChatGPT input field; update if ChatGPT's UI changes.
CHATGPT_INPUT_SELECTOR: 'textarea[data-id="root"]'
};
/***********************************************************************
* CSS Styles and UI Layout
***********************************************************************/
const styles = `
/* General transitions */
.prompt-manager, .clip-toggle, .prompt-content, .prompt-bottom-actions,
.prompt-header, .prompt-title, .prompt-close, .prompt-toast, .prompt-card,
.prompt-preview, .prompt-actions, .prompt-btn, .prompt-search, .prompt-controls,
.prompt-edit-icon, .prompt-save-icon, .prompt-drag-handle, .prompt-theme-toggle,
.prompt-tabs, .prompt-tab, .prompt-tags, .prompt-import, .prompt-export {
transition: all 0.2s ease;
}
/* Manager container */
.prompt-manager {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 640px;
height: 720px;
background: #1a1b1e;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 99999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
flex-direction: column;
opacity: 0;
visibility: hidden;
user-select: none;
resize: both;
overflow: hidden;
}
.prompt-manager.open {
opacity: 1;
visibility: visible;
}
/* Floating Toggle Button (exact as original) */
.clip-toggle {
position: fixed;
right: 340px;
top: 10px;
background: #2c2d31;
border: none;
border-radius: 12px;
padding: 12px;
cursor: pointer;
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
}
.clip-toggle.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.clip-toggle:hover {
transform: scale(1.05);
background: #3a3b3f;
}
/* Header and Tabs */
.prompt-header {
padding: 10px 20px;
color: #fff;
border-bottom: 1px solid #2c2d31;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
}
.prompt-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.prompt-close {
background: transparent;
border: none;
color: #6b7280;
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
}
.prompt-close:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.prompt-tabs {
display: flex;
gap: 12px;
padding: 0 20px;
border-bottom: 1px solid #2c2d31;
background: #24252a;
}
.prompt-tab {
cursor: pointer;
padding: 8px 12px;
color: #ccc;
border-bottom: 2px solid transparent;
}
.prompt-tab.active {
color: #fff;
border-color: #98c379;
}
/* Controls: Search, Theme Toggle, Import/Export */
.prompt-controls {
margin: 10px 20px;
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
.prompt-search {
background: #2c2d31;
color: #fff;
border: 1px solid #3a3b3f;
padding: 8px 12px;
border-radius: 6px;
flex: 1;
}
.prompt-theme-toggle, .prompt-import, .prompt-export {
background: #5a67d8;
color: #fff;
padding: 8px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
}
.prompt-theme-toggle:hover, .prompt-import:hover, .prompt-export:hover {
background: #6875f5;
}
/* Content Area */
.prompt-content {
flex: 1;
overflow-y: auto;
padding: 10px 20px;
margin-bottom: 60px;
}
.prompt-content::-webkit-scrollbar {
width: 6px;
}
.prompt-content::-webkit-scrollbar-track {
background: #1a1b1e;
}
.prompt-content::-webkit-scrollbar-thumb {
background: #2c2d31;
border-radius: 3px;
}
.prompt-content::-webkit-scrollbar-thumb:hover {
background: #3a3b3f;
}
/* Bottom Actions */
.prompt-bottom-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #2c2d31;
border-radius: 0 0 16px 16px;
}
.prompt-save, .prompt-save-clipboard, .prompt-clear-all {
height: 40px;
padding: 0 24px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
line-height: 40px;
white-space: nowrap;
border: none;
}
.prompt-save {
background: #98c379;
color: #1a1b1e;
box-shadow: 0 2px 8px rgba(152, 195, 121, 0.2);
}
.prompt-save:hover {
background: #a9d389;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(152, 195, 121, 0.3);
}
.prompt-save:disabled {
background: #4a4b4f;
cursor: not-allowed;
}
.prompt-save-clipboard {
background: #5a67d8;
color: #fff;
box-shadow: 0 2px 8px rgba(90, 103, 216, 0.2);
}
.prompt-save-clipboard:hover {
background: #6875f5;
}
.prompt-clear-all {
background: #dc2626;
color: #fff;
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.2);
padding-right: 20px;
text-align: center;
}
.prompt-clear-all:hover {
background: #ef4444;
}
.prompt-clear-all.confirm {
background: #991b1b;
}
/* Prompt Card Styles */
.prompt-card {
background: #2c2d31;
border-radius: 12px;
padding: 12px;
margin-bottom: 12px;
color: #fff;
border: 1px solid #3a3b3f;
}
.prompt-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #4a4b4f;
}
.prompt-title-wrapper {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.prompt-title-display {
font-size: 16px;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.prompt-edit-icon, .prompt-save-icon {
background: transparent;
border: none;
color: #6b7280;
font-size: 16px;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
}
.prompt-edit-icon:hover, .prompt-save-icon:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.prompt-preview {
font-size: 14px;
color: #d1d5db;
margin: 4px 0 8px 0;
line-height: 1.5;
max-height: 90px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.prompt-info {
font-size: 12px;
color: #9ca3af;
margin-bottom: 4px;
}
.prompt-tags {
margin-top: 4px;
font-size: 12px;
}
.prompt-tags span {
color: #98c379;
cursor: pointer;
margin-right: 6px;
}
/* Action buttons within each prompt card */
.prompt-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
border-top: 1px solid #3a3b3f;
padding-top: 8px;
margin-top: 4px;
}
.prompt-btn {
height: 32px;
padding: 0 12px;
background: transparent;
border: 1px solid #4a4b4f;
color: #fff;
border-radius: 6px;
font-size: 13px;
line-height: 30px;
}
.prompt-btn:hover {
background: #3a3b3f;
border-color: #5a5b5f;
}
.prompt-btn.delete {
color: #ef4444;
border-color: #ef4444;
}
.prompt-btn.delete:hover {
background: rgba(239, 68, 68, 0.1);
}
/* Rating stars */
.prompt-rating {
display: flex;
gap: 4px;
align-items: center;
margin-right: auto;
}
.prompt-rating-star {
cursor: pointer;
color: #ccc;
}
.prompt-rating-star.filled {
color: #ffc107;
}
/* Resize drag handle */
.prompt-drag-handle {
position: absolute;
right: 2px;
bottom: 2px;
width: 16px;
height: 16px;
cursor: nwse-resize;
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
/* Toast notification styling */
.prompt-toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 10px 16px;
border-radius: 8px;
z-index: 999999;
}
/* Light theme overrides */
.prompt-manager[data-theme="light"] {
background: #ffffff;
color: #1a1b1e;
}
.prompt-manager[data-theme="light"] .prompt-search {
background: #f0f0f0;
color: #1a1b1e;
}
.prompt-manager[data-theme="light"] .prompt-card {
background: #f8f8f8;
color: #1a1b1e;
}
`;
// Append the stylesheet to the document head.
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
/***********************************************************************
* UI Elements: Manager Window & Floating Toggle Button
***********************************************************************/
const manager = document.createElement('div');
manager.className = 'prompt-manager';
manager.dataset.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// Manager HTML layout with header, tabs, controls, content, and bottom actions.
manager.innerHTML = `
<div class="prompt-header">
<h2 class="prompt-title">GPT Prompt Manager</h2>
<button class="prompt-close">×</button>
</div>
<div class="prompt-tabs">
${CONFIG.TABS.map(tab => `<div class="prompt-tab" data-tab="${tab}">${tab}</div>`).join('')}
</div>
<div class="prompt-controls">
<input type="text" class="prompt-search" placeholder="Search prompts by title, content, or tag...">
<button class="prompt-theme-toggle">Toggle Theme</button>
<button class="prompt-import">Import</button>
<button class="prompt-export">Export</button>
</div>
<div class="prompt-content"></div>
<div class="prompt-bottom-actions">
<button class="prompt-save-clipboard">Save Clipboard</button>
<button class="prompt-save" disabled>Save Selection</button>
<button class="prompt-clear-all">Clear All</button>
</div>
<div class="prompt-drag-handle"></div>
`;
// Floating toggle button using the exact SVG and styling from the original script.
const toggleBtn = document.createElement('button');
toggleBtn.className = 'clip-toggle';
toggleBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<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>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
</svg>
`;
/***********************************************************************
* GPT Prompt Manager Class Definition
***********************************************************************/
class GptPromptManager {
constructor() {
this.prompts = this.loadPrompts();
this.archivedPrompts = this.loadArchivedPrompts();
this.isOpen = false;
this.clearAllTimeout = null;
this.searchTerm = '';
this.activeTab = 'General';
this.manualTheme = null; // null indicates following system preference
this.activeTagFilter = '';
// Append UI elements
document.body.appendChild(manager);
document.body.appendChild(toggleBtn);
// Initialize events, theme detection, drag/resize, and initial render.
this.initEvents();
this.setupThemeDetection();
this.makeDraggable();
this.renderTabs();
this.renderPrompts();
this.updateSaveButton();
}
/***********************************************************************
* Data Persistence: Loading & Saving Prompts
***********************************************************************/
loadPrompts() {
try {
const saved = typeof GM_getValue !== 'undefined'
? GM_getValue(CONFIG.STORAGE_KEY)
: localStorage.getItem(CONFIG.STORAGE_KEY);
return saved ? JSON.parse(saved) : [];
} catch (err) {
console.error('Error loading prompts:', err);
return [];
}
}
loadArchivedPrompts() {
try {
const archived = typeof GM_getValue !== 'undefined'
? GM_getValue(CONFIG.STORAGE_KEY + '_archived')
: localStorage.getItem(CONFIG.STORAGE_KEY + '_archived');
return archived ? JSON.parse(archived) : [];
} catch (err) {
console.error('Error loading archived prompts:', err);
return [];
}
}
savePrompts() {
try {
const data = JSON.stringify(this.prompts);
if (typeof GM_setValue !== 'undefined') {
GM_setValue(CONFIG.STORAGE_KEY, data);
} else {
localStorage.setItem(CONFIG.STORAGE_KEY, data);
}
} catch (err) {
console.error('Error saving prompts:', err);
this.showToast('Error saving prompts');
}
}
saveArchivedPrompts() {
try {
const data = JSON.stringify(this.archivedPrompts);
if (typeof GM_setValue !== 'undefined') {
GM_setValue(CONFIG.STORAGE_KEY + '_archived', data);
} else {
localStorage.setItem(CONFIG.STORAGE_KEY + '_archived', data);
}
} catch (err) {
console.error('Error saving archived prompts:', err);
}
}
/***********************************************************************
* Theme Detection and Toggling
***********************************************************************/
setupThemeDetection() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', e => {
if (this.manualTheme === null) {
manager.dataset.theme = e.matches ? 'dark' : 'light';
}
});
}
/***********************************************************************
* Event Binding
***********************************************************************/
initEvents() {
// Toggle manager visibility
toggleBtn.addEventListener('click', () => this.toggle());
manager.querySelector('.prompt-close').addEventListener('click', () => this.close());
// Save selected text as a new prompt
document.addEventListener('selectionchange', () => this.updateSaveButton());
manager.querySelector('.prompt-save').addEventListener('click', () => {
const selection = window.getSelection().toString().trim();
if (selection) {
this.addPrompt(selection);
}
});
// Save clipboard text as a new prompt
manager.querySelector('.prompt-save-clipboard').addEventListener('click', async () => {
try {
const clipboardText = await navigator.clipboard.readText();
if (clipboardText.trim()) {
this.addPrompt(clipboardText);
} else {
this.showToast('Clipboard is empty');
}
} catch (err) {
console.error('Clipboard error:', err);
this.showToast('Failed to read clipboard');
}
});
// Clear all prompts (with confirmation)
manager.querySelector('.prompt-clear-all').addEventListener('click', e => {
this.handleClearAll(e.target);
});
// Tab switching
manager.querySelectorAll('.prompt-tab').forEach(tab => {
tab.addEventListener('click', () => {
this.activeTab = tab.dataset.tab;
this.activeTagFilter = '';
this.renderTabs();
this.renderPrompts();
});
});
// Search functionality
manager.querySelector('.prompt-search').addEventListener('input', e => {
this.searchTerm = e.target.value.toLowerCase();
this.activeTagFilter = '';
this.renderPrompts();
});
// Theme toggle
manager.querySelector('.prompt-theme-toggle').addEventListener('click', () => {
if (this.manualTheme === 'dark') {
this.manualTheme = 'light';
} else if (this.manualTheme === 'light') {
this.manualTheme = 'dark';
} else {
this.manualTheme = (manager.dataset.theme === 'dark') ? 'light' : 'dark';
}
manager.dataset.theme = this.manualTheme;
});
// Import and Export prompts
manager.querySelector('.prompt-import').addEventListener('click', () => this.importPrompts());
manager.querySelector('.prompt-export').addEventListener('click', () => this.exportPrompts());
// Global keyboard shortcut: Ctrl+Alt+P
document.addEventListener('keydown', e => {
if (e.ctrlKey === CONFIG.KEYBOARD_SHORTCUT.ctrlKey &&
e.altKey === CONFIG.KEYBOARD_SHORTCUT.altKey &&
e.key.toLowerCase() === CONFIG.KEYBOARD_SHORTCUT.key) {
e.preventDefault();
this.toggle();
const selection = window.getSelection().toString().trim();
if (selection) {
this.addPrompt(selection);
}
}
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
});
// Delegate click events for prompt card actions
manager.querySelector('.prompt-content').addEventListener('click', e => {
const card = e.target.closest('.prompt-card');
if (!card) return;
const id = parseInt(card.dataset.id);
if (e.target.classList.contains('prompt-edit-icon') ||
e.target.classList.contains('prompt-save-icon')) {
this.toggleEditTitle(id, card);
} else if (e.target.classList.contains('delete')) {
if (this.activeTab === 'Archive') {
this.deleteArchivedPrompt(id);
} else {
this.removePrompt(id);
}
} else if (e.target.classList.contains('favorite')) {
this.toggleFavorite(id);
} else if (e.target.classList.contains('move-up')) {
e.stopPropagation();
this.movePrompt(id, -1);
} else if (e.target.classList.contains('move-down')) {
e.stopPropagation();
this.movePrompt(id, 1);
} else if (e.target.classList.contains('copy')) {
const text = card.querySelector('.prompt-preview').textContent;
this.copyText(text);
} else if (e.target.classList.contains('insert')) {
const text = card.querySelector('.prompt-preview').textContent;
this.insertIntoChatGpt(text);
this.incrementUsage(id);
} else if (e.target.classList.contains('prompt-rating-star')) {
const starValue = parseInt(e.target.dataset.value);
this.setRating(id, starValue);
} else if (e.target.tagName === 'SPAN' && e.target.parentElement.classList.contains('prompt-tags')) {
// Filter by tag when clicked
this.activeTagFilter = e.target.textContent.slice(1).toLowerCase();
manager.querySelector('.prompt-search').value = '';
this.searchTerm = '';
this.renderPrompts();
}
});
}
/***********************************************************************
* Draggable & Resizable Manager
***********************************************************************/
makeDraggable() {
const header = manager.querySelector('.prompt-header');
header.addEventListener('mousedown', e => {
e.preventDefault();
const offsetX = e.clientX - manager.offsetLeft;
const offsetY = e.clientY - manager.offsetTop;
const onMouseMove = ev => {
manager.style.left = (ev.clientX - offsetX) + 'px';
manager.style.top = (ev.clientY - offsetY) + 'px';
};
const onMouseUp = () => document.removeEventListener('mousemove', onMouseMove);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp, { once: true });
});
// Enable resizing using the drag handle
const dragHandle = manager.querySelector('.prompt-drag-handle');
let isResizing = false, startX, startY, initialWidth, initialHeight;
dragHandle.addEventListener('mousedown', e => {
e.preventDefault();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
initialWidth = manager.offsetWidth;
initialHeight = manager.offsetHeight;
const onMouseMove = ev => {
if (isResizing) {
const newWidth = initialWidth + (ev.clientX - startX);
const newHeight = initialHeight + (ev.clientY - startY);
manager.style.width = newWidth + 'px';
manager.style.height = newHeight + 'px';
}
};
const onMouseUp = () => {
isResizing = false;
document.removeEventListener('mousemove', onMouseMove);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp, { once: true });
});
}
/***********************************************************************
* Prompt Operations: Add, Remove, and Modify Prompts
***********************************************************************/
addPrompt(text) {
const timestamp = Date.now();
const category = this.activeTab !== 'Archive' ? this.activeTab : 'General';
const promptItem = {
id: timestamp,
title: `Prompt-${timestamp}`,
text: text.trim(),
date: new Date().toISOString(),
url: window.location.href,
isFavorite: false,
category,
tags: [],
usageCount: 0,
rating: 0
};
// Insert at the beginning of non-favorite items.
const index = this.prompts.findIndex(p => !p.isFavorite);
this.prompts.splice(index === -1 ? this.prompts.length : index, 0, promptItem);
// Enforce maximum prompt limit and archive older items if needed.
if (this.prompts.length > CONFIG.MAX_ITEMS) {
const nonFavorites = this.prompts.filter(p => !p.isFavorite);
if (nonFavorites.length) {
const itemToArchive = nonFavorites[nonFavorites.length - 1];
this.archivedPrompts.unshift(itemToArchive);
this.prompts = this.prompts.filter(p => p.id !== itemToArchive.id);
this.saveArchivedPrompts();
}
}
this.savePrompts();
this.renderPrompts();
this.showToast('Prompt saved');
}
removePrompt(id) {
this.prompts = this.prompts.filter(p => p.id !== id);
// Attempt to restore one archived prompt if available.
if (this.archivedPrompts.length && this.prompts.length < CONFIG.MAX_ITEMS) {
const restoreItem = this.archivedPrompts.find(p => !p.isFavorite);
if (restoreItem) {
this.prompts.push(restoreItem);
this.archivedPrompts = this.archivedPrompts.filter(p => p.id !== restoreItem.id);
this.saveArchivedPrompts();
this.showToast('Restored a prompt from archive');
}
}
this.savePrompts();
this.renderPrompts();
this.showToast('Prompt deleted');
}
deleteArchivedPrompt(id) {
this.archivedPrompts = this.archivedPrompts.filter(p => p.id !== id);
this.saveArchivedPrompts();
this.renderPrompts();
this.showToast('Archived prompt permanently deleted');
}
movePrompt(id, direction) {
const idx = this.prompts.findIndex(p => p.id === id);
if (idx === -1) return;
const newIdx = idx + direction;
if (newIdx < 0 || newIdx >= this.prompts.length) {
this.showToast('Cannot move prompt further');
return;
}
const item = this.prompts[idx];
const target = this.prompts[newIdx];
// Prevent moving across favorite boundaries.
if ((item.isFavorite && !target.isFavorite) || (!item.isFavorite && target.isFavorite)) {
this.showToast('Cannot move across favorite sections');
return;
}
this.prompts.splice(idx, 1);
this.prompts.splice(newIdx, 0, item);
this.savePrompts();
this.renderPrompts();
}
toggleFavorite(id) {
const item = this.prompts.find(p => p.id === id);
if (!item) return;
const favoriteCount = this.prompts.filter(p => p.isFavorite).length;
if (!item.isFavorite && favoriteCount >= CONFIG.MAX_FAVORITES) {
this.showToast(`Maximum ${CONFIG.MAX_FAVORITES} favorites allowed`);
return;
}
const idx = this.prompts.indexOf(item);
this.prompts.splice(idx, 1);
item.isFavorite = !item.isFavorite;
if (item.isFavorite) {
const lastFav = this.prompts.findLastIndex(p => p.isFavorite);
this.prompts.splice(lastFav + 1, 0, item);
} else {
const firstNonFav = this.prompts.findIndex(p => !p.isFavorite);
this.prompts.splice(firstNonFav === -1 ? this.prompts.length : firstNonFav, 0, item);
}
this.savePrompts();
this.renderPrompts();
this.showToast(item.isFavorite ? 'Marked as favorite' : 'Removed favorite');
}
copyText(text) {
navigator.clipboard.writeText(text)
.then(() => this.showToast('Copied to clipboard'))
.catch(err => {
console.error('Copy error:', err);
this.showToast('Failed to copy text');
});
}
insertIntoChatGpt(text) {
const inputEl = document.querySelector(CONFIG.CHATGPT_INPUT_SELECTOR);
if (inputEl) {
inputEl.value = text;
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
this.showToast('Prompt inserted');
} else {
this.showToast('ChatGPT input field not found');
}
}
incrementUsage(id) {
const item = this.prompts.find(p => p.id === id);
if (!item) return;
item.usageCount = (item.usageCount || 0) + 1;
this.savePrompts();
this.renderPrompts();
}
setRating(id, value) {
const item = this.prompts.find(p => p.id === id);
if (!item) return;
item.rating = value;
this.savePrompts();
this.renderPrompts();
}
/***********************************************************************
* Edit Title and Tag Management
***********************************************************************/
toggleEditTitle(id, card) {
const titleEl = card.querySelector('.prompt-title-display');
const editIcon = card.querySelector('.prompt-edit-icon');
const saveIcon = card.querySelector('.prompt-save-icon');
const item = this.getItemById(id);
if (!item) return;
if (titleEl.contentEditable !== 'true') {
// Enable editing; show title and tags
card.classList.add('editing');
titleEl.contentEditable = 'true';
titleEl.textContent = item.title + (item.tags.length ? `, ${item.tags.join(', ')}` : '');
titleEl.focus();
// Place the cursor at the end
const range = document.createRange();
range.selectNodeContents(titleEl);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
editIcon.style.display = 'none';
saveIcon.style.display = 'inline-block';
} else {
// Save changes to title and tags
const newText = titleEl.textContent.trim();
if (!newText) {
this.showToast('Title cannot be blank');
return;
}
card.classList.remove('editing');
titleEl.contentEditable = 'false';
editIcon.style.display = 'inline-block';
saveIcon.style.display = 'none';
const [newTitle, ...tagParts] = newText.split(',');
item.title = newTitle.trim();
item.tags = tagParts.map(t => t.trim()).filter(t => t !== '');
this.savePrompts();
this.renderPrompts();
}
}
getItemById(id) {
return this.prompts.find(p => p.id === id) ||
this.archivedPrompts.find(p => p.id === id);
}
/***********************************************************************
* Clear All Prompts with Confirmation
***********************************************************************/
handleClearAll(button) {
if (button.classList.contains('confirm')) {
this.prompts = [];
this.archivedPrompts = [];
this.savePrompts();
this.saveArchivedPrompts();
this.renderPrompts();
this.showToast('All prompts cleared');
button.textContent = 'Clear All';
button.classList.remove('confirm');
if (this.clearAllTimeout) {
clearTimeout(this.clearAllTimeout);
this.clearAllTimeout = null;
}
} else {
button.textContent = 'Confirm Clear?';
button.classList.add('confirm');
if (this.clearAllTimeout) clearTimeout(this.clearAllTimeout);
this.clearAllTimeout = setTimeout(() => {
button.textContent = 'Clear All';
button.classList.remove('confirm');
this.clearAllTimeout = null;
}, CONFIG.CONFIRM_TIMEOUT);
}
}
/***********************************************************************
* Render Tabs and Prompt List
***********************************************************************/
renderTabs() {
manager.querySelectorAll('.prompt-tab').forEach(tabEl => {
tabEl.classList.toggle('active', tabEl.dataset.tab === this.activeTab);
});
}
renderPrompts() {
const content = manager.querySelector('.prompt-content');
content.innerHTML = '';
let itemsToRender = (this.activeTab === 'Archive')
? this.archivedPrompts
: this.prompts.filter(p => p.category === this.activeTab);
// Filter by search term and active tag
itemsToRender = itemsToRender.filter(item => {
const searchMatch = item.title.toLowerCase().includes(this.searchTerm) ||
item.text.toLowerCase().includes(this.searchTerm);
const tagMatch = this.activeTagFilter
? item.tags.map(t => t.toLowerCase()).includes(this.activeTagFilter)
: true;
return searchMatch && tagMatch;
});
// Sort: favorites first, then by rating and usage
itemsToRender.sort((a, b) => {
if (b.isFavorite !== a.isFavorite) return b.isFavorite ? 1 : -1;
if (b.rating !== a.rating) return b.rating - a.rating;
return b.usageCount - a.usageCount;
});
if (!itemsToRender.length) {
content.innerHTML = `<div class="prompt-empty">
${(this.searchTerm || this.activeTagFilter) ? 'No matching prompts found' : 'No prompts saved yet'}
</div>`;
return;
}
// Build HTML for each prompt card.
content.innerHTML = itemsToRender.map(item => {
const stars = [1,2,3,4,5].map(value => {
const filled = value <= item.rating ? 'filled' : '';
return `<span class="prompt-rating-star ${filled}" data-value="${value}">★</span>`;
}).join('');
return `
<div class="prompt-card ${item.isFavorite ? 'favorite' : ''}" data-id="${item.id}">
<div class="prompt-title-wrapper">
<button class="prompt-edit-icon">✎</button>
<div class="prompt-title-display">${item.title}</div>
<button class="prompt-save-icon" style="display:none;">💾</button>
</div>
<div class="prompt-preview">${item.text}</div>
<div class="prompt-info">
<span class="prompt-date">${new Date(item.date).toLocaleDateString()}</span>
<br>
<span class="prompt-url">[<a href="${item.url}" target="_blank">source</a>]</span>
</div>
<div class="prompt-tags">
${item.tags.map(tag => `<span>#${tag}</span>`).join('')}
</div>
<div class="prompt-actions">
<div class="prompt-rating">${stars}</div>
<button class="prompt-btn favorite">${item.isFavorite ? '★' : '☆'}</button>
<button class="prompt-btn copy">Copy</button>
<button class="prompt-btn insert">Insert</button>
<button class="prompt-btn delete">Delete</button>
${this.activeTab === 'Archive'
? ''
: `<button class="prompt-btn move-up">↑</button>
<button class="prompt-btn move-down">↓</button>`
}
</div>
</div>
`;
}).join('');
}
/***********************************************************************
* Update Save Button Based on Selection
***********************************************************************/
updateSaveButton() {
const saveBtn = manager.querySelector('.prompt-save');
if (!saveBtn) return;
const selection = window.getSelection();
saveBtn.disabled = !(selection && selection.toString().trim().length > 0);
}
/***********************************************************************
* Open/Close Manager & Toast Notifications
***********************************************************************/
toggle() {
this.isOpen = !this.isOpen;
manager.classList.toggle('open');
// Do not hide the toggle icon once visible
// (Remove 'hidden' class if present)
toggleBtn.classList.remove('hidden');
}
close() {
this.isOpen = false;
manager.classList.remove('open');
// Keep the toggle icon visible
toggleBtn.classList.remove('hidden');
}
showToast(message) {
const existing = document.querySelector('.prompt-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'prompt-toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), CONFIG.TOAST_DURATION);
}
/***********************************************************************
* Import/Export Functionality
***********************************************************************/
importPrompts() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'application/json';
fileInput.addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = evt => {
try {
const data = JSON.parse(evt.target.result);
if (Array.isArray(data.prompts)) {
data.prompts.forEach(item => this.prompts.push(item));
}
if (Array.isArray(data.archivedPrompts)) {
data.archivedPrompts.forEach(item => this.archivedPrompts.push(item));
}
this.savePrompts();
this.saveArchivedPrompts();
this.renderPrompts();
this.showToast('Prompts imported successfully');
} catch (err) {
console.error('Import error:', err);
this.showToast('Failed to import file');
}
};
reader.readAsText(file);
});
fileInput.click();
}
exportPrompts() {
const data = {
prompts: this.prompts,
archivedPrompts: this.archivedPrompts
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'gpt-prompts-export.json';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 0);
}
}
// Instantiate the GPT Prompt Manager.
new GptPromptManager();
})();