Skip to content

Instantly share code, notes, and snippets.

@dinh
Created November 14, 2024 15:54
Show Gist options
  • Save dinh/39dc41d8da78ee517dcd0b3226011d51 to your computer and use it in GitHub Desktop.
Save dinh/39dc41d8da78ee517dcd0b3226011d51 to your computer and use it in GitHub Desktop.
Prompt Manager - Windsurf - V4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<title>AI Prompt Manager</title>
</head>
<body>
<div class="container">
<header>
<h1>AI Prompt Manager</h1>
<div class="header-controls">
<button id="themeToggle" class="icon-btn" title="Toggle theme">
<i class="fas fa-sun"></i>
<i class="fas fa-moon"></i>
</button>
<div class="search-bar">
<i class="fas fa-search search-icon"></i>
<input type="text" id="searchInput" placeholder="Search prompts... (Press '/' to focus)">
</div>
</div>
</header>
<div id="overlay" class="overlay"></div>
<div class="main-content">
<aside class="sidebar">
<div class="filters">
<h3>Categories</h3>
<select id="categoryFilter">
<option value="">All Categories</option>
<option value="general">General</option>
<option value="coding">Coding</option>
<option value="writing">Writing</option>
<option value="creative">Creative</option>
</select>
<div class="popular-tags">
<h3>Popular Tags</h3>
<div id="popularTags" class="tags-cloud"></div>
</div>
<div class="actions">
<button id="newPromptBtn" class="primary-btn">
<i class="fas fa-plus"></i> New Prompt
</button>
<button id="exportBtn" class="secondary-btn">
<i class="fas fa-file-export"></i> Export
</button>
<input type="file" id="importInput" accept=".json" style="display: none;">
<button id="importBtn" class="secondary-btn">
<i class="fas fa-file-import"></i> Import
</button>
</div>
</div>
</aside>
<main class="content">
<div class="bulk-actions" id="bulkActions">
<div class="bulk-actions-left">
<span id="selectedCount">0 items selected</span>
<button class="clear-selection" id="clearSelection">Clear selection</button>
</div>
<div class="bulk-actions-right">
<button class="bulk-delete" id="bulkDelete">
<i class="fas fa-trash-alt"></i> Delete
</button>
<button id="bulkMove">
<i class="fas fa-folder-open"></i> Move to
</button>
<button id="bulkExport">
<i class="fas fa-file-export"></i> Export
</button>
</div>
</div>
<!-- Prompt Form -->
<div id="promptForm" class="prompt-form">
<div class="form-header">
<h2 id="formTitle">New Prompt</h2>
<button id="closeFormBtn" class="icon-btn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="form-group">
<label for="promptTitle">Title</label>
<input type="text" id="promptTitle" placeholder="Enter prompt title" autocomplete="off">
<div class="error-message" id="titleError"></div>
</div>
<div class="form-group">
<label for="promptInput">Prompt</label>
<div class="prompt-input-container">
<textarea id="promptInput" rows="5" placeholder="Enter your prompt here. Use {{variableName}} for template variables."></textarea>
<div class="template-toolbar">
<button type="button" class="icon-btn" id="addVariableBtn" title="Add variable">
<i class="fas fa-plus"></i> Add Variable
</button>
<button type="button" class="icon-btn" id="previewTemplateBtn" title="Preview template">
<i class="fas fa-eye"></i> Preview
</button>
</div>
</div>
<div class="template-variables" id="templateVariables"></div>
<div class="error-message" id="promptError"></div>
</div>
<div class="form-group">
<label for="promptCategory">Category</label>
<select id="promptCategory">
<option value="">Select a category</option>
<option value="general">General</option>
<option value="coding">Coding</option>
<option value="writing">Writing</option>
<option value="creative">Creative</option>
</select>
<div class="error-message" id="categoryError"></div>
</div>
<div class="form-group">
<label for="promptTags">Tags</label>
<div class="tag-input-container">
<div class="tag-display"></div>
<input type="text" id="promptTags" placeholder="Add tags (comma separated)">
</div>
<div class="tag-suggestions"></div>
<div class="error-message" id="tagsError"></div>
</div>
<div class="form-actions">
<button type="button" id="cancelBtn" class="secondary-btn">Cancel</button>
<button type="button" id="submitBtn" class="primary-btn">Save Prompt</button>
</div>
</div>
<div class="prompt-list" id="promptList">
<template id="promptTemplate">
<div class="prompt-card">
<div class="prompt-header">
<h3 class="prompt-title"></h3>
<div class="prompt-actions">
<button class="icon-btn copy-btn" title="Copy prompt">
<i class="fas fa-copy"></i>
</button>
<button class="icon-btn edit-btn" title="Edit prompt">
<i class="fas fa-edit"></i>
</button>
<button class="icon-btn delete-btn" title="Delete prompt">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="prompt-content"></div>
<div class="prompt-footer">
<div class="prompt-tags"></div>
<div class="prompt-category">
<i class="fas fa-folder"></i>
<span></span>
</div>
</div>
</div>
</template>
</div>
</main>
</div>
</div>
<!-- Modal Template -->
<template id="modalTemplate">
<div class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title"></h3>
<button class="modal-close icon-btn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-content"></div>
<div class="modal-footer"></div>
</div>
</div>
</template>
<!-- Toast Template -->
<template id="toastTemplate">
<div class="toast">
<div class="toast-content">
<i class="toast-icon"></i>
<span class="toast-message"></span>
</div>
<button class="toast-close">
<i class="fas fa-times"></i>
</button>
</div>
</template>
<div id="toastContainer" class="toast-container"></div>
<script src="app.js" type="module"></script>
</body>
</html>
class PromptManager {
constructor() {
this.loadFromStorage();
}
loadFromStorage() {
try {
const stored = localStorage.getItem("prompts");
this.prompts = stored ? JSON.parse(stored) : {};
// Validate and repair prompts data structure
Object.entries(this.prompts).forEach(([id, prompt]) => {
if (!prompt || typeof prompt !== "object") {
delete this.prompts[id];
return;
}
// Ensure all required fields exist with default values
this.prompts[id] = {
id: Number(id),
title: prompt.title || "",
content: prompt.content || "",
category: prompt.category || "General",
tags: Array.isArray(prompt.tags) ? prompt.tags : [],
template: prompt.template || { variables: {}, isTemplate: false },
createdAt: prompt.createdAt || new Date().toISOString(),
updatedAt:
prompt.updatedAt || prompt.createdAt || new Date().toISOString()
};
});
// Calculate next ID safely
const ids = Object.keys(this.prompts)
.map(Number)
.filter((id) => !isNaN(id));
this.nextId = ids.length > 0 ? Math.max(...ids) + 1 : 1;
// Save the repaired data structure
this.saveToStorage();
} catch (error) {
console.error("Error loading prompts:", error);
this.prompts = {};
this.nextId = 1;
}
}
saveToStorage() {
localStorage.setItem("prompts", JSON.stringify(this.prompts));
}
addPrompt(promptData) {
const id = this.nextId++;
this.prompts[id] = {
...promptData,
tags: promptData.tags || [], // Ensure tags is always an array
template: promptData.template || { variables: {}, isTemplate: false },
id,
createdAt: new Date().toISOString()
};
this.saveToStorage();
return id;
}
updatePrompt(id, updatedPrompt) {
if (!this.prompts[id]) {
console.error("Prompt not found:", id);
return false;
}
// Preserve the ID and template data when updating
this.prompts[id] = {
...updatedPrompt,
template: updatedPrompt.template ||
this.prompts[id].template || { variables: {}, isTemplate: false },
id,
updatedAt: new Date().toISOString()
};
this.saveToStorage();
return true;
}
deletePrompt(id) {
if (this.prompts[id]) {
delete this.prompts[id];
this.saveToStorage();
return true;
}
return false;
}
deletePrompts(promptIds) {
let deletedCount = 0;
this.prompts = Object.fromEntries(
Object.entries(this.prompts).filter(([id, prompt]) => {
if (promptIds.includes(Number(id))) {
deletedCount++;
return false;
}
return true;
})
);
this.saveToStorage();
return deletedCount;
}
getPrompt(id) {
return this.prompts[id];
}
getAllPrompts() {
return Object.values(this.prompts);
}
searchPrompts(query) {
query = query.toLowerCase();
return Object.values(this.prompts).filter(
(prompt) =>
prompt.title.toLowerCase().includes(query) ||
prompt.content.toLowerCase().includes(query) ||
(prompt.tags || []).some((tag) => tag.toLowerCase().includes(query))
);
}
filterByCategory(category) {
if (!category) return this.getAllPrompts();
return Object.values(this.prompts).filter(
(prompt) => prompt.category === category
);
}
movePrompts(promptIds, category) {
let movedCount = 0;
this.prompts = Object.fromEntries(
Object.entries(this.prompts).map(([id, prompt]) => {
if (promptIds.includes(Number(id))) {
movedCount++;
return [id, { ...prompt, category }];
}
return [id, prompt];
})
);
this.saveToStorage();
return movedCount;
}
getCategories() {
const categories = new Set(
Object.values(this.prompts).map((prompt) => prompt.category)
);
return Array.from(categories).sort();
}
exportPrompts() {
return JSON.stringify(this.prompts, null, 2);
}
importPrompts(jsonData) {
try {
const importedPrompts = JSON.parse(jsonData);
// Ensure all imported prompts have tags array
Object.values(importedPrompts).forEach((prompt) => {
prompt.tags = prompt.tags || [];
});
this.prompts = { ...this.prompts, ...importedPrompts };
this.nextId = Math.max(...Object.keys(this.prompts).map(Number)) + 1;
this.saveToStorage();
return true;
} catch (error) {
console.error("Error importing prompts:", error);
return false;
}
}
getFilteredPrompts(searchQuery = "", categoryFilter = "") {
let filteredPrompts = Object.values(this.prompts);
// Apply search filter
if (searchQuery) {
const query = searchQuery.toLowerCase();
filteredPrompts = filteredPrompts.filter(
(prompt) =>
prompt.title.toLowerCase().includes(query) ||
prompt.content.toLowerCase().includes(query) ||
(prompt.tags &&
prompt.tags.some((tag) => tag.toLowerCase().includes(query))) ||
(prompt.category && prompt.category.toLowerCase().includes(query))
);
}
// Apply category filter
if (categoryFilter) {
filteredPrompts = filteredPrompts.filter(
(prompt) =>
prompt.category &&
prompt.category.toLowerCase() === categoryFilter.toLowerCase()
);
}
// Sort by creation date (newest first)
return filteredPrompts.sort((a, b) => {
const dateA = a.createdAt ? new Date(a.createdAt) : new Date(0);
const dateB = b.createdAt ? new Date(b.createdAt) : new Date(0);
return dateB - dateA;
});
}
}
class ToastManager {
constructor() {
this.container = document.getElementById("toastContainer");
}
show(message, type = "success") {
const template = document.getElementById("toastTemplate");
const toast = template.content.cloneNode(true).querySelector(".toast");
toast.classList.add(type);
const icon = toast.querySelector(".toast-icon");
icon.classList.add("fas");
icon.classList.add(
type === "success" ? "fa-check-circle" : "fa-exclamation-circle"
);
toast.querySelector(".toast-message").textContent = message;
toast.querySelector(".toast-close").addEventListener("click", () => {
toast.remove();
});
this.container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
}
class Modal {
constructor() {
this.template = document.getElementById("modalTemplate");
}
show(title, content, buttons) {
const modal = this.template.content
.cloneNode(true)
.querySelector(".modal-overlay");
modal.querySelector(".modal-title").textContent = title;
modal.querySelector(".modal-content").innerHTML = content;
const footer = modal.querySelector(".modal-footer");
buttons.forEach((button) => {
const btn = document.createElement("button");
btn.textContent = button.text;
btn.className = button.primary ? "primary-btn" : "secondary-btn";
btn.addEventListener("click", () => {
button.onClick();
modal.remove();
});
footer.appendChild(btn);
});
modal.querySelector(".modal-close").addEventListener("click", () => {
modal.remove();
});
document.body.appendChild(modal);
}
confirm(title, message, confirmText, type) {
return new Promise((resolve) => {
const modal = this.template.content
.cloneNode(true)
.querySelector(".modal-overlay");
modal.querySelector(".modal-title").textContent = title;
modal.querySelector(".modal-content").textContent = message;
const footer = modal.querySelector(".modal-footer");
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancel";
cancelBtn.className = "secondary-btn";
cancelBtn.addEventListener("click", () => {
modal.remove();
resolve(false);
});
footer.appendChild(cancelBtn);
const confirmBtn = document.createElement("button");
confirmBtn.textContent = confirmText;
confirmBtn.className = `${type}-btn`;
confirmBtn.addEventListener("click", () => {
modal.remove();
resolve(true);
});
footer.appendChild(confirmBtn);
modal.querySelector(".modal-close").addEventListener("click", () => {
modal.remove();
resolve(false);
});
document.body.appendChild(modal);
});
}
}
class App {
constructor() {
this.promptManager = new PromptManager();
this.toastManager = new ToastManager();
this.modal = new Modal();
this.template = document.getElementById("modalTemplate");
this.currentPage = 1;
this.itemsPerPage = 10;
this.selectedPrompts = new Set();
this.elements = {};
this.editingPromptId = null;
this.initializeElements();
this.attachEventListeners();
this.setupKeyboardShortcuts();
this.initializeTheme();
this.renderPrompts();
}
initializeElements() {
this.elements = {
promptList: document.getElementById("promptList"),
promptTemplate: document.getElementById("promptTemplate"),
newPromptBtn: document.getElementById("newPromptBtn"),
promptForm: document.getElementById("promptForm"),
closeFormBtn: document.getElementById("closeFormBtn"),
submitBtn: document.getElementById("submitBtn"),
cancelBtn: document.getElementById("cancelBtn"),
searchInput: document.getElementById("searchInput"),
categoryFilter: document.getElementById("categoryFilter"),
themeToggle: document.getElementById("themeToggle"),
promptTitle: document.getElementById("promptTitle"),
promptInput: document.getElementById("promptInput"),
promptCategory: document.getElementById("promptCategory"),
tagInput: document.getElementById("promptTags"),
tagDisplay: document.querySelector(".tag-display"),
tagSuggestions: document.querySelector(".tag-suggestions"),
bulkActions: document.getElementById("bulkActions"),
selectedCount: document.getElementById("selectedCount"),
clearSelection: document.getElementById("clearSelection"),
bulkDelete: document.getElementById("bulkDelete"),
bulkMove: document.getElementById("bulkMove"),
bulkExport: document.getElementById("bulkExport"),
formTitle: document.getElementById("formTitle"),
popularTags: document.getElementById("popularTags"),
addVariableBtn: document.getElementById("addVariableBtn"),
previewTemplateBtn: document.getElementById("previewTemplateBtn"),
templateVariables: document.getElementById("templateVariables"),
titleError: document.getElementById("titleError"),
promptError: document.getElementById("promptError"),
tagsError: document.getElementById("tagsError"),
categoryError: document.getElementById("categoryError")
};
}
attachEventListeners() {
// Form controls
this.elements.newPromptBtn.addEventListener("click", () =>
this.showPromptForm()
);
this.elements.closeFormBtn.addEventListener("click", () =>
this.hidePromptForm()
);
this.elements.cancelBtn.addEventListener("click", () =>
this.hidePromptForm()
);
this.elements.submitBtn.addEventListener("click", () =>
this.handleSubmit()
);
// Form input handlers
this.elements.tagInput.addEventListener("keydown", (e) =>
this.handleTagInput(e)
);
// Search and filter
this.elements.searchInput.addEventListener("input", () => {
this.currentPage = 1;
this.renderPrompts(
this.elements.searchInput.value,
this.elements.categoryFilter.value
);
});
this.elements.categoryFilter.addEventListener("change", () => {
this.currentPage = 1;
this.renderPrompts(
this.elements.searchInput.value,
this.elements.categoryFilter.value
);
});
// Theme toggle
this.elements.themeToggle.addEventListener("click", () =>
this.toggleTheme()
);
// Bulk operations
this.elements.clearSelection.addEventListener("click", () =>
this.clearSelection()
);
this.elements.bulkDelete.addEventListener("click", () =>
this.handleBulkDelete()
);
this.elements.bulkMove.addEventListener("click", () =>
this.handleBulkMove()
);
this.elements.bulkExport.addEventListener("click", () =>
this.handleBulkExport()
);
// Tag input handling
this.elements.tagInput.addEventListener("input", () =>
this.handleTagInput()
);
this.elements.tagInput.addEventListener("keydown", (e) =>
this.handleTagKeydown(e)
);
this.elements.tagDisplay.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-tag")) {
const tag = e.target.closest(".tag");
if (tag) {
this.removeTag(tag);
}
}
});
// Template functionality
this.elements.addVariableBtn.addEventListener("click", () =>
this.addTemplateVariable()
);
this.elements.previewTemplateBtn.addEventListener("click", () =>
this.previewTemplate()
);
this.elements.promptInput.addEventListener("input", () =>
this.detectTemplateVariables()
);
}
setupKeyboardShortcuts() {
document.addEventListener("keydown", (e) => {
if (e.key === "/" && !this.isInputFocused()) {
e.preventDefault();
this.elements.searchInput.focus();
}
if (e.key === "n" && e.ctrlKey && !this.isInputFocused()) {
e.preventDefault();
this.showPromptForm();
}
if (e.key === "Escape") {
this.hidePromptForm();
}
});
}
isInputFocused() {
const activeElement = document.activeElement;
return (
activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA"
);
}
showPromptForm() {
this.elements.promptForm.classList.add("active");
this.elements.formTitle.textContent = this.editingPromptId
? "Edit Prompt"
: "New Prompt";
this.elements.submitBtn.textContent = this.editingPromptId
? "Update"
: "Add";
this.elements.promptTitle.focus();
this.clearFormErrors();
}
hidePromptForm() {
this.elements.promptForm.classList.remove("active");
this.clearForm();
}
clearForm() {
this.elements.promptTitle.value = "";
this.elements.promptInput.value = "";
this.elements.promptCategory.value = "";
this.elements.tagInput.value = "";
this.elements.templateVariables.innerHTML = "";
this.editingPromptId = null;
this.elements.formTitle.textContent = "New Prompt";
this.elements.submitBtn.textContent = "Add";
this.clearFormErrors();
}
clearFormErrors() {
const errorElements = [
this.elements.titleError,
this.elements.promptError,
this.elements.tagsError,
this.elements.categoryError
];
document.querySelectorAll(".form-group").forEach((group) => {
group.classList.remove("has-error");
});
errorElements.forEach((element) => {
if (element) {
element.textContent = "";
element.style.display = "none";
}
});
}
showFormError(field, message) {
const errorMap = {
title: this.elements.titleError,
prompt: this.elements.promptError,
tags: this.elements.tagsError,
category: this.elements.categoryError
};
const errorElement = errorMap[field];
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = "block";
const formGroup = errorElement.closest(".form-group");
if (formGroup) {
formGroup.classList.add("has-error");
formGroup.querySelector("input, textarea, select")?.focus();
}
}
}
validateForm() {
this.clearFormErrors();
let isValid = true;
const title = this.elements.promptTitle.value.trim();
if (!title) {
this.showFormError("title", "Please enter a title");
isValid = false;
} else if (title.length < 3) {
this.showFormError("title", "Title must be at least 3 characters");
isValid = false;
}
const prompt = this.elements.promptInput.value.trim();
if (!prompt) {
this.showFormError("prompt", "Please enter a prompt");
isValid = false;
} else if (prompt.length < 10) {
this.showFormError("prompt", "Prompt must be at least 10 characters");
isValid = false;
}
const category = this.elements.promptCategory.value;
if (!category) {
this.showFormError("category", "Please select a category");
isValid = false;
}
return isValid;
}
handleSubmit() {
if (!this.validateForm()) {
return;
}
const variables = Array.from(
this.elements.templateVariables.children
).reduce((acc, varEl) => {
const name = varEl.querySelector(".variable-name").value;
const defaultValue = varEl.querySelector(".variable-default").value;
if (name) {
acc[name] = defaultValue;
}
return acc;
}, {});
const promptData = {
title: this.elements.promptTitle.value.trim(),
content: this.elements.promptInput.value.trim(),
category: this.elements.promptCategory.value,
tags: this.getTags(),
template: {
variables,
isTemplate: Object.keys(variables).length > 0
}
};
if (this.editingPromptId) {
this.promptManager.updatePrompt(this.editingPromptId, promptData);
this.toastManager.show("Prompt updated successfully");
} else {
this.promptManager.addPrompt(promptData);
this.toastManager.show("Prompt added successfully");
}
this.hidePromptForm();
this.renderPrompts();
}
handleTagInput(event) {
if (event.key === "Enter" || event.key === ",") {
event.preventDefault();
const tagText = this.elements.tagInput.value.trim();
if (tagText) {
this.addTag(tagText);
this.elements.tagInput.value = "";
}
} else if (
event.key === "Backspace" &&
this.elements.tagInput.value === ""
) {
// Remove the last tag when backspace is pressed on empty input
const tags = this.elements.tagDisplay.querySelectorAll(".tag");
if (tags.length > 0) {
tags[tags.length - 1].remove();
}
}
}
handleTagKeydown(e) {
const input = this.elements.tagInput;
const value = input.value.trim();
if (e.key === "Enter" && value) {
e.preventDefault();
this.addTag(value);
input.value = "";
this.hideTagSuggestions();
} else if (e.key === "Backspace" && !value) {
e.preventDefault();
const tags = this.elements.tagDisplay.querySelectorAll(".tag");
if (tags.length) {
this.removeTag(tags[tags.length - 1]);
}
}
}
addTag(tagText) {
const normalizedTag = tagText.toLowerCase().trim();
if (!normalizedTag || this.hasTag(normalizedTag)) return;
const tag = document.createElement("div");
tag.className = "tag";
tag.innerHTML = `
<span class="tag-text">${this.escapeHtml(normalizedTag)}</span>
<span class="remove-tag">×</span>
`;
this.elements.tagDisplay.appendChild(tag);
this.updateTagInput();
}
removeTag(tagElement) {
tagElement.remove();
this.updateTagInput();
}
hasTag(tagText) {
const tags = this.elements.tagDisplay.querySelectorAll(".tag");
return Array.from(tags).some(
(tag) =>
tag.querySelector(".tag-text").textContent.toLowerCase() ===
tagText.toLowerCase()
);
}
getTags() {
return Array.from(
this.elements.tagDisplay.querySelectorAll(".tag")
).map((tag) => tag.querySelector(".tag-text").textContent.trim());
}
updateTagInput() {
const tags = this.getTags();
this.elements.tagInput.value = "";
// Update hidden input or form state if needed
}
showTagSuggestions(query) {
const suggestions = this.getTagSuggestions(query);
if (!suggestions.length) {
this.hideTagSuggestions();
return;
}
const suggestionsList = suggestions
.map((tag) => `<div class="tag-suggestion">${this.escapeHtml(tag)}</div>`)
.join("");
this.elements.tagSuggestions.innerHTML = suggestionsList;
this.elements.tagSuggestions.classList.add("active");
// Add click handlers for suggestions
this.elements.tagSuggestions
.querySelectorAll(".tag-suggestion")
.forEach((suggestion) => {
suggestion.addEventListener("click", () => {
this.addTag(suggestion.textContent);
this.elements.tagInput.value = "";
this.hideTagSuggestions();
});
});
}
hideTagSuggestions() {
this.elements.tagSuggestions.classList.remove("active");
}
getTagSuggestions(query) {
// Get existing tags from all prompts
const allTags = new Set();
Object.values(this.promptManager.getAllPrompts()).forEach((prompt) => {
(prompt.tags || []).forEach((tag) => allTags.add(tag.toLowerCase()));
});
// Filter and sort suggestions
return Array.from(allTags)
.filter((tag) => tag.includes(query.toLowerCase()))
.sort()
.slice(0, 5);
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
setTags(tags) {
this.elements.tagDisplay.innerHTML = "";
tags.forEach((tag) => this.addTag(tag));
}
handleEdit(prompt) {
this.editingPromptId = prompt.id;
this.elements.promptTitle.value = prompt.title;
this.elements.promptInput.value = prompt.content;
this.elements.promptCategory.value = prompt.category;
this.setTags(prompt.tags || []);
// Clear existing template variables
this.elements.templateVariables.innerHTML = "";
// If this is a template prompt, restore its variables
if (prompt.template?.isTemplate && prompt.template.variables) {
Object.entries(prompt.template.variables).forEach(
([name, defaultValue]) => {
this.addTemplateVariableWithName(name);
// Find the last added variable element and set its default value
const varElement = this.elements.templateVariables.lastElementChild;
if (varElement) {
varElement.querySelector(".variable-default").value =
defaultValue || "";
}
}
);
}
this.showPromptForm();
}
handleDelete(promptId) {
this.modal
.confirm(
"Delete Prompt",
"Are you sure you want to delete this prompt?",
"Delete",
"danger"
)
.then((confirmed) => {
if (confirmed) {
this.promptManager.deletePrompt(promptId);
this.toastManager.show("Prompt deleted successfully");
this.renderPrompts();
}
});
}
async handleCopy(content) {
try {
await navigator.clipboard.writeText(content);
this.toastManager.show("Prompt copied to clipboard!");
} catch (err) {
console.error("Failed to copy text: ", err);
this.toastManager.show("Failed to copy prompt to clipboard", "error");
}
}
async applyPromptToActiveElement(prompt) {
// Find the active text field in the webpage
const activeElement = document.activeElement;
const promptContent = prompt.content || prompt.promptInput || "";
try {
// First try to use the Clipboard API to paste the content
await navigator.clipboard.writeText(promptContent);
// If we have an active element that can receive text
if (
activeElement &&
(activeElement instanceof HTMLTextAreaElement ||
activeElement instanceof HTMLInputElement ||
activeElement.isContentEditable)
) {
// For contentEditable elements
if (activeElement.isContentEditable) {
activeElement.focus();
document.execCommand("paste");
}
// For input/textarea elements
else if ("value" in activeElement) {
const start = activeElement.selectionStart || 0;
const end = activeElement.selectionEnd || start;
const currentValue = activeElement.value;
// Insert the prompt content
activeElement.value =
currentValue.substring(0, start) +
promptContent +
currentValue.substring(end);
// Move cursor to end of inserted text
if (typeof activeElement.setSelectionRange === "function") {
const newPosition = start + promptContent.length;
activeElement.setSelectionRange(newPosition, newPosition);
}
}
// Trigger input event
activeElement.dispatchEvent(new Event("input", { bubbles: true }));
activeElement.dispatchEvent(new Event("change", { bubbles: true }));
// Try to simulate Enter key press if it's a chat input
if (
activeElement.getAttribute("role") === "textbox" ||
activeElement.classList.contains("chat-input") ||
activeElement.placeholder?.toLowerCase().includes("message") ||
activeElement.placeholder?.toLowerCase().includes("chat")
) {
activeElement.dispatchEvent(
new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true
})
);
}
}
this.toastManager.show(
"Prompt content copied and ready to paste!",
"success"
);
} catch (error) {
console.error("Error applying prompt:", error);
this.toastManager.show(
"Failed to apply prompt. The content is in your clipboard - please paste manually.",
"warning"
);
}
}
renderPromptCard(prompt) {
try {
if (!prompt || typeof prompt !== "object") {
console.error("Invalid prompt data:", prompt);
return null;
}
// Get the template from our stored elements
const template = this.elements.promptTemplate;
if (!template) {
console.error(
"Prompt template not found in DOM. Make sure the page is fully loaded."
);
return null;
}
const card = template.content
.cloneNode(true)
?.querySelector(".prompt-card");
if (!card) {
console.error("Prompt card element not found in template");
return null;
}
// Set prompt ID
card.dataset.id = prompt.id;
// Set title with fallback
const titleElement = card.querySelector(".prompt-title");
if (titleElement) {
titleElement.textContent = prompt.title || "Untitled Prompt";
}
// Set content with fallback
const contentElement = card.querySelector(".prompt-content");
if (contentElement) {
contentElement.textContent =
prompt.content || prompt.promptInput || "No content";
}
// Set category with fallback
const categorySpan = card.querySelector(".prompt-category span");
if (categorySpan) {
categorySpan.textContent = prompt.category || "Uncategorized";
}
// Add tags if they exist
const tagsContainer = card.querySelector(".prompt-tags");
if (tagsContainer && Array.isArray(prompt.tags)) {
prompt.tags.forEach((tag) => {
if (tag && typeof tag === "string") {
const tagElement = document.createElement("span");
tagElement.className = "tag";
tagElement.textContent = tag;
tagsContainer.appendChild(tagElement);
}
});
}
// Setup action buttons
const actionsContainer = card.querySelector(".prompt-actions");
if (actionsContainer) {
// Add "Apply" button first
const applyBtn = document.createElement("button");
applyBtn.className = "icon-btn apply-btn";
applyBtn.title = "Apply prompt";
applyBtn.innerHTML = '<i class="fas fa-check"></i>';
applyBtn.addEventListener("click", async (e) => {
e.stopPropagation();
try {
// Always try to apply directly first
await this.applyPromptToActiveElement(prompt);
this.toastManager.show("Prompt applied successfully!");
} catch (error) {
console.error("Error applying prompt:", error);
this.toastManager.show(
"Failed to apply prompt. Make sure a text field is focused.",
"error"
);
}
});
actionsContainer.insertBefore(applyBtn, actionsContainer.firstChild);
// Add "Use Template" button if it's a template
if (prompt.template?.isTemplate) {
const useTemplateBtn = document.createElement("button");
useTemplateBtn.className = "icon-btn use-template-btn";
useTemplateBtn.title = "Fill template variables";
useTemplateBtn.innerHTML = '<i class="fas fa-file-import"></i>';
useTemplateBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.useTemplate(prompt);
});
actionsContainer.insertBefore(
useTemplateBtn,
actionsContainer.firstChild
);
}
// Copy button
const copyBtn = card.querySelector(".copy-btn");
if (copyBtn) {
copyBtn.addEventListener("click", (e) => {
e.stopPropagation();
const contentToCopy = prompt.content || prompt.promptInput || "";
navigator.clipboard.writeText(contentToCopy).then(() => {
this.toastManager.show("Prompt copied to clipboard!");
});
});
}
// Edit button
const editBtn = card.querySelector(".edit-btn");
if (editBtn) {
editBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.handleEdit(prompt);
});
}
// Delete button
const deleteBtn = card.querySelector(".delete-btn");
if (deleteBtn) {
deleteBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.handleDelete(prompt.id);
});
}
}
// Add click handler for selection
card.addEventListener("click", (e) => {
if (!e.target.closest("button")) {
this.togglePromptSelection(prompt.id);
}
});
return card;
} catch (error) {
console.error("Error rendering prompt card:", error);
return null;
}
}
updateBulkActionsUI() {
const selectedCount = this.selectedPrompts.size;
if (selectedCount > 0) {
this.elements.bulkActions.classList.add("visible");
this.elements.selectedCount.textContent = `${selectedCount} item${
selectedCount !== 1 ? "s" : ""
} selected`;
} else {
this.elements.bulkActions.classList.remove("visible");
}
}
togglePromptSelection(promptId) {
const prompt = document.querySelector(`[data-id="${promptId}"]`);
if (!prompt) return;
if (this.selectedPrompts.has(promptId)) {
this.selectedPrompts.delete(promptId);
prompt.classList.remove("selected");
} else {
this.selectedPrompts.add(promptId);
prompt.classList.add("selected");
}
this.updateBulkActionsUI();
}
clearSelection() {
this.selectedPrompts.clear();
document.querySelectorAll(".prompt-card.selected").forEach((card) => {
card.classList.remove("selected");
});
this.updateBulkActionsUI();
}
handleBulkDelete() {
if (this.selectedPrompts.size === 0) return;
const selectedPromptIds = Array.from(this.selectedPrompts);
const message = `Are you sure you want to delete ${
selectedPromptIds.length
} prompt${selectedPromptIds.length !== 1 ? "s" : ""}?`;
this.modal.show("Confirm Delete", message, [
{
text: "Cancel",
primary: false,
onClick: () => {}
},
{
text: "Delete",
primary: true,
onClick: () => {
const count = selectedPromptIds.length;
selectedPromptIds.forEach((promptId) => {
this.promptManager.deletePrompt(promptId);
});
this.clearSelection();
this.renderPrompts();
this.toastManager.show(
`Deleted ${count} prompt${count !== 1 ? "s" : ""}`
);
}
}
]);
}
handleBulkMove() {
if (this.selectedPrompts.size === 0) return;
const categories = ["general", "coding", "writing", "creative"];
const categoryOptions = categories
.map(
(category) =>
`<option value="${category}">${
category.charAt(0).toUpperCase() + category.slice(1)
}</option>`
)
.join("");
const content = `
<div class="form-group">
<label for="moveCategory">Select Category</label>
<select id="moveCategory">
${categoryOptions}
</select>
</div>
`;
this.modal.show("Move Prompts", content, [
{
text: "Cancel",
primary: false,
onClick: () => {}
},
{
text: "Move",
primary: true,
onClick: () => {
const category = document.getElementById("moveCategory").value;
this.selectedPrompts.forEach((promptId) => {
const prompt = this.promptManager.getPrompt(promptId);
if (prompt) {
prompt.category = category;
this.promptManager.updatePrompt(promptId, prompt);
}
});
this.clearSelection();
this.renderPrompts();
this.toastManager.show(
`Moved ${this.selectedPrompts.size} prompt${
this.selectedPrompts.size !== 1 ? "s" : ""
} to ${category}`
);
}
}
]);
}
handleBulkExport() {
if (this.selectedPrompts.size === 0) return;
const prompts = Array.from(this.selectedPrompts)
.map((promptId) => this.promptManager.getPrompt(promptId))
.filter(Boolean);
const blob = new Blob([JSON.stringify(prompts, null, 2)], {
type: "application/json"
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `prompts_export_${
new Date().toISOString().split("T")[0]
}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.toastManager.show(
`Exported ${this.selectedPrompts.size} prompt${
this.selectedPrompts.size !== 1 ? "s" : ""
}`
);
}
handleSearch() {
const searchQuery = this.elements.searchInput.value.trim();
const categoryFilter = this.elements.categoryFilter.value;
// Reset to first page when searching
this.currentPage = 1;
// Update UI
this.renderPrompts(searchQuery, categoryFilter);
}
handleCategoryFilter() {
const searchQuery = this.elements.searchInput.value.trim();
const categoryFilter = this.elements.categoryFilter.value;
this.currentPage = 1;
this.renderPrompts(searchQuery, categoryFilter);
}
renderPrompts(searchQuery = "", category = "") {
// Clear existing prompts
this.elements.promptList.innerHTML = "";
// Get filtered prompts
const filteredPrompts = this.filterPrompts(searchQuery, category);
// Calculate pagination
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const paginatedPrompts = filteredPrompts.slice(startIndex, endIndex);
// Render prompts
paginatedPrompts.forEach((prompt) => {
const promptElement = this.renderPromptCard(prompt);
if (promptElement) {
this.elements.promptList.appendChild(promptElement);
}
});
this.renderPagination(filteredPrompts.length);
this.updatePopularTags();
}
filterPrompts(searchQuery = "", category = "") {
try {
let prompts = Object.values(this.promptManager.getAllPrompts());
// Filter by category if specified
if (category) {
prompts = prompts.filter((prompt) => prompt.category === category);
}
// If no search query, return category-filtered results
if (!searchQuery.trim()) {
return prompts;
}
// Split search terms and identify tag searches
const searchTerms = searchQuery.trim().toLowerCase().split(/\s+/);
const tagTerms = searchTerms
.filter((term) => term.startsWith("#"))
.map((tag) => tag.slice(1));
const textTerms = searchTerms.filter((term) => !term.startsWith("#"));
// Filter prompts that match all conditions
return prompts.filter((prompt) => {
if (!prompt) return false;
// Check tags first (if any tag terms exist)
if (tagTerms.length > 0) {
const promptTags = (prompt.tags || []).map((tag) =>
tag.toLowerCase()
);
const hasAllTags = tagTerms.every((tag) => promptTags.includes(tag));
if (!hasAllTags) return false;
}
// If no text terms, we're done
if (textTerms.length === 0) return true;
// Check text content
const searchableContent = [
prompt.title?.toLowerCase() || "",
prompt.content?.toLowerCase() || "",
(prompt.tags || []).join(" ").toLowerCase()
].join(" ");
// All text terms must match
return textTerms.every((term) => searchableContent.includes(term));
});
} catch (error) {
console.error("Error filtering prompts:", error);
return [];
}
}
renderPagination(totalPrompts) {
const paginationDiv = document.createElement("div");
paginationDiv.className = "pagination";
// Previous button
const prevButton = document.createElement("button");
prevButton.textContent = "← Previous";
prevButton.disabled = this.currentPage === 1;
prevButton.addEventListener("click", () => {
if (this.currentPage > 1) {
this.currentPage--;
this.renderPrompts();
}
});
// Next button
const nextButton = document.createElement("button");
nextButton.textContent = "Next →";
nextButton.disabled =
this.currentPage === Math.ceil(totalPrompts / this.itemsPerPage);
nextButton.addEventListener("click", () => {
if (this.currentPage < Math.ceil(totalPrompts / this.itemsPerPage)) {
this.currentPage++;
this.renderPrompts();
}
});
// Page info
const pageInfo = document.createElement("span");
pageInfo.className = "page-info";
pageInfo.textContent = `Page ${this.currentPage} of ${Math.ceil(
totalPrompts / this.itemsPerPage
)} (${totalPrompts} prompts)`;
paginationDiv.appendChild(prevButton);
paginationDiv.appendChild(pageInfo);
paginationDiv.appendChild(nextButton);
this.elements.promptList.appendChild(paginationDiv);
}
updatePopularTags() {
// Get all tags and their frequencies
const tagFrequency = new Map();
Object.values(this.promptManager.getAllPrompts()).forEach((prompt) => {
(prompt.tags || []).forEach((tag) => {
tagFrequency.set(tag, (tagFrequency.get(tag) || 0) + 1);
});
});
// Sort tags by frequency and get top 10
const popularTags = Array.from(tagFrequency.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
// Clear existing tags
this.elements.popularTags.innerHTML = "";
// Create and append tag elements
popularTags.forEach(([tag, count]) => {
const tagElement = document.createElement("div");
tagElement.className = "tag";
const tagText = `#${tag}`;
// Check if tag is in search
const isActive = this.isTagInSearch(tagText);
if (isActive) {
tagElement.classList.add("active");
}
tagElement.innerHTML = `
<span class="tag-text">${this.escapeHtml(tag)}</span>
<span class="tag-count">${count}</span>
`;
// Add click handler for filtering
tagElement.addEventListener("click", () => {
this.toggleSearchTag(tagText);
tagElement.classList.toggle("active");
});
this.elements.popularTags.appendChild(tagElement);
});
}
isTagInSearch(tagText) {
const searchTerms = this.elements.searchInput.value.trim().split(/\s+/);
return searchTerms.includes(tagText);
}
toggleSearchTag(tagText) {
const searchInput = this.elements.searchInput;
const currentSearch = searchInput.value.trim();
const searchTerms = currentSearch.split(/\s+/).filter(Boolean);
if (this.isTagInSearch(tagText)) {
// Remove tag
const newTerms = searchTerms.filter((term) => term !== tagText);
searchInput.value = newTerms.join(" ");
} else {
// Add tag
searchInput.value = currentSearch
? `${currentSearch} ${tagText}`
: tagText;
}
// Trigger search
searchInput.dispatchEvent(new Event("input"));
}
initializeTheme() {
const savedTheme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", savedTheme);
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "light" ? "dark" : "light";
requestAnimationFrame(() => {
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
});
}
addTemplateVariable() {
const variableId = `var_${Date.now()}`;
const variableElement = document.createElement("div");
variableElement.className = "template-variable";
variableElement.dataset.id = variableId;
variableElement.innerHTML = `
<input type="text" placeholder="Variable name"
class="variable-name" value="variable${
this.getTemplateVariablesCount() + 1
}">
<input type="text" placeholder="Default value" class="variable-default">
<i class="fas fa-times remove-variable" title="Remove variable"></i>
`;
// Add click handler for remove button
variableElement
.querySelector(".remove-variable")
.addEventListener("click", () => {
variableElement.remove();
this.updatePromptTemplate();
});
// Add input handlers for variable name
const nameInput = variableElement.querySelector(".variable-name");
nameInput.addEventListener("input", () => {
this.updatePromptTemplate();
});
this.elements.templateVariables.appendChild(variableElement);
this.updatePromptTemplate();
}
getTemplateVariablesCount() {
return this.elements.templateVariables.children.length;
}
detectTemplateVariables() {
const promptText = this.elements.promptInput.value;
const variables = promptText.match(/\{\{([^}]+)\}\}/g) || [];
// Create a set of existing variable names
const existingVars = new Set(
Array.from(this.elements.templateVariables.children).map(
(el) => el.querySelector(".variable-name").value
)
);
// Add missing variables
variables.forEach((match) => {
const varName = match.slice(2, -2).trim();
if (!existingVars.has(varName)) {
this.addTemplateVariableWithName(varName);
}
});
}
addTemplateVariableWithName(name) {
const variableId = `var_${Date.now()}`;
const variableElement = document.createElement("div");
variableElement.className = "template-variable";
variableElement.dataset.id = variableId;
variableElement.innerHTML = `
<input type="text" placeholder="Variable name"
class="variable-name" value="${this.escapeHtml(name)}">
<input type="text" placeholder="Default value" class="variable-default">
<i class="fas fa-times remove-variable" title="Remove variable"></i>
`;
variableElement
.querySelector(".remove-variable")
.addEventListener("click", () => {
variableElement.remove();
});
this.elements.templateVariables.appendChild(variableElement);
}
updatePromptTemplate() {
const promptText = this.elements.promptInput.value;
const variables = Array.from(this.elements.templateVariables.children);
let updatedText = promptText;
variables.forEach((varEl) => {
const name = varEl.querySelector(".variable-name").value;
if (name) {
const regex = new RegExp(`\\{\\{${name}\\}\\}`, "g");
if (!promptText.match(regex)) {
const cursorPos = this.elements.promptInput.selectionStart;
updatedText =
promptText.slice(0, cursorPos) +
`{{${name}}}` +
promptText.slice(cursorPos);
this.elements.promptInput.value = updatedText;
this.elements.promptInput.setSelectionRange(
cursorPos + name.length + 4,
cursorPos + name.length + 4
);
}
}
});
}
previewTemplate() {
const promptText = this.elements.promptInput.value;
const variables = Array.from(
this.elements.templateVariables.children
).reduce((acc, varEl) => {
const name = varEl.querySelector(".variable-name").value;
const defaultValue = varEl.querySelector(".variable-default").value;
if (name) {
acc[name] = defaultValue;
}
return acc;
}, {});
let previewText = promptText;
Object.entries(variables).forEach(([name, value]) => {
const regex = new RegExp(`\\{\\{${name}\\}\\}`, "g");
previewText = previewText.replace(regex, value || `[${name}]`);
});
const content = `
<div class="preview-content">${this.escapeHtml(previewText)}</div>
<div class="preview-variables">
${Object.entries(variables)
.map(
([name, value]) => `
<div class="preview-variable">
<strong>${this.escapeHtml(name)}:</strong> ${
this.escapeHtml(value) || "[empty]"
}
</div>
`
)
.join("")}
</div>
`;
this.modal.show("Template Preview", content, [
{
text: "Close",
primary: true,
onClick: () => {}
}
]);
}
async useTemplate(prompt) {
if (!prompt?.content) {
this.toastManager.show("Invalid template", "error");
return;
}
// Extract variables from the template
const variables = this.extractTemplateVariables(prompt.content);
if (variables.length === 0) {
// If no variables, just apply the template directly
await this.applyPromptToActiveElement(prompt);
return;
}
// Create form fields for each variable
let formHTML = '<div class="template-form">';
variables.forEach((variable) => {
formHTML += `
<div class="form-group">
<label for="${variable}">${this.formatVariableName(variable)}</label>
<textarea id="${variable}" rows="3" placeholder="Enter value for ${this.formatVariableName(
variable
)}"></textarea>
</div>
`;
});
formHTML += `
<div class="template-preview">
<h4>Preview:</h4>
<pre class="preview-content"></pre>
</div>
</div>`;
// Show modal with variable form
this.modal.show("Fill Template Variables", formHTML, [
{
text: "Cancel",
primary: false,
onClick: () => {}
},
{
text: "Apply Template",
primary: true,
onClick: async () => {
// Get values for all variables
const values = {};
variables.forEach((variable) => {
values[variable] = document.getElementById(variable).value.trim();
});
// Replace variables in template
let filledContent = prompt.content;
Object.entries(values).forEach(([variable, value]) => {
filledContent = filledContent.replace(
new RegExp(`{{${variable}}}`, "g"),
value
);
});
// Apply the filled template
await this.applyPromptToActiveElement({
...prompt,
content: filledContent
});
}
}
]);
// Add live preview
const previewContent = document.querySelector(".preview-content");
if (previewContent) {
const updatePreview = () => {
let preview = prompt.content;
variables.forEach((variable) => {
const value =
document.getElementById(variable)?.value.trim() ||
`{{${variable}}}`;
preview = preview.replace(new RegExp(`{{${variable}}}`, "g"), value);
});
previewContent.textContent = preview;
};
// Update preview on input
variables.forEach((variable) => {
const input = document.getElementById(variable);
if (input) {
input.addEventListener("input", updatePreview);
}
});
// Initial preview
updatePreview();
}
}
extractTemplateVariables(content) {
const matches = content.match(/{{([^}]+)}}/g) || [];
return [...new Set(matches.map((match) => match.slice(2, -2).trim()))];
}
formatVariableName(variable) {
return variable
.split(/[_\s]+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
}
}
// Initialize the application
document.addEventListener("DOMContentLoaded", () => {
new App();
});
:root {
/* Light theme variables */
--primary-color: #4a90e2;
--secondary-color: #28a745;
--danger-color: #dc3545;
--background-color: #f4f4f4;
--card-background: #ffffff;
--text-color: #333333;
--label-color: #666666;
--placeholder-color: #999999;
--border-color: #e0e0e0;
--hover-color: #f8f9fa;
--shadow-color: rgba(0, 0, 0, 0.1);
--tag-bg: #e9ecef;
--tag-hover: #dee2e6;
--primary-color-rgb: 74, 144, 226;
/* Common variables */
--border-radius: 8px;
--spacing: 20px;
--transition-speed: 0.3s;
--shadow: 0 2px 10px var(--shadow-color);
--form-shadow: 0 4px 6px var(--shadow-color);
--input-focus-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
--error-color: #dc3545;
}
[data-theme="dark"] {
--primary-color: #5c9ce6;
--secondary-color: #2fb344;
--danger-color: #e4405f;
--background-color: #1a1a1a;
--card-background: #2d2d2d;
--text-color: #e0e0e0;
--label-color: #b0b0b0;
--placeholder-color: #808080;
--border-color: #404040;
--hover-color: #363636;
--shadow-color: rgba(0, 0, 0, 0.3);
--tag-bg: #404040;
--tag-hover: #4a4a4a;
--primary-color-rgb: 92, 156, 230;
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #ffffff;
--border-color: #404040;
--hover-color: #2a2a2a;
--primary-color: #4a9eff;
--secondary-color: #666666;
--card-bg: #2d2d2d;
--input-bg: #333333;
--modal-bg: #2d2d2d;
--toast-bg: #333333;
--toast-text: #ffffff;
--shadow-color: rgba(0, 0, 0, 0.3);
}
[data-theme="dark"] .prompt-card {
background-color: var(--card-bg);
border-color: var(--border-color);
}
[data-theme="dark"] input,
[data-theme="dark"] textarea,
[data-theme="dark"] select {
background-color: var(--input-bg);
color: var(--text-color);
border-color: var(--border-color);
}
[data-theme="dark"] .modal {
background-color: var(--modal-bg);
border-color: var(--border-color);
}
[data-theme="dark"] .tag {
background-color: var(--secondary-color);
color: var(--text-color);
}
[data-theme="dark"] .fa-sun {
display: inline-block;
}
[data-theme="dark"] .fa-moon {
display: none;
}
/* Theme transition */
body {
font-family: "Segoe UI", Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
transition: background-color var(--transition-speed) ease,
color var(--transition-speed) ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.prompt-card,
.prompt-form,
input,
textarea,
select,
button,
.tag,
.modal,
.toast {
transition: background-color var(--transition-speed) ease,
color var(--transition-speed) ease,
border-color var(--transition-speed) ease,
box-shadow var(--transition-speed) ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing);
}
/* Header Styles */
header {
display: flex;
align-items: center;
padding: var(--spacing);
background-color: var(--card-background);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
margin-bottom: var(--spacing);
}
.header-controls {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
justify-content: flex-end;
margin-left: 2rem;
}
/* Theme toggle styles */
#themeToggle {
position: relative;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: none;
color: var(--text-color);
cursor: pointer;
transition: all var(--transition-speed) ease;
}
#themeToggle:hover {
background-color: var(--hover-color);
}
#themeToggle .fa-sun,
#themeToggle .fa-moon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: opacity var(--transition-speed) ease;
}
[data-theme="light"] #themeToggle .fa-sun {
opacity: 0;
}
[data-theme="light"] #themeToggle .fa-moon {
opacity: 1;
}
[data-theme="dark"] #themeToggle .fa-sun {
opacity: 1;
}
[data-theme="dark"] #themeToggle .fa-moon {
opacity: 0;
}
h1 {
margin: 0;
color: var(--primary-color);
font-size: 2em;
white-space: nowrap;
}
.search-bar {
position: relative;
flex: 1;
max-width: 500px;
min-width: 300px;
}
.search-bar input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--card-background);
color: var(--text-color);
font-size: 1rem;
transition: all var(--transition-speed) ease;
}
.search-bar input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: var(--input-focus-shadow);
}
.search-bar .search-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--label-color);
pointer-events: none;
}
/* Main Content Layout */
.main-content {
display: grid;
grid-template-columns: 250px 1fr;
gap: var(--spacing);
}
/* Sidebar Styles */
.sidebar {
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.sidebar h3 {
margin-bottom: 10px;
color: var(--text-color);
}
.filters {
margin-bottom: 2rem;
}
.filters h3 {
color: var(--text-color);
font-size: 1.1rem;
margin-bottom: 1rem;
font-weight: 600;
}
/* Select Element Base Styles */
select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666666' d='M6 8.825L1.175 4 2.238 2.938 6 6.7l3.763-3.762L10.825 4z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 1rem center;
padding-right: 2.5rem !important;
cursor: pointer;
}
/* Form Group Select Specific Styles */
.form-group select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--card-background);
color: var(--text-color);
font-size: 1rem;
font-family: inherit;
transition: all var(--transition-speed) ease;
}
.form-group select:hover {
border-color: var(--primary-color);
}
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: var(--input-focus-shadow);
}
/* Sidebar Select Specific Styles */
.sidebar select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--card-background);
color: var(--text-color);
font-size: 1rem;
font-family: inherit;
transition: all var(--transition-speed) ease;
margin-bottom: 1.5rem;
}
.sidebar select:hover {
border-color: var(--primary-color);
}
.sidebar select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: var(--input-focus-shadow);
}
select option {
padding: 0.5rem;
background: var(--card-background);
color: var(--text-color);
}
/* Form Styles */
.prompt-form {
display: none;
background: var(--card-background);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--form-shadow);
max-width: 800px;
margin: 0 auto;
}
.prompt-form.active {
display: block;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.form-header h2 {
margin: 0;
color: var(--text-color);
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-color);
font-weight: 500;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--card-background);
color: var(--text-color);
font-size: 1rem;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: var(--input-focus-shadow);
}
.form-group textarea {
min-height: 150px;
resize: vertical;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 2rem;
}
.error-message {
color: var(--error-color);
font-size: 0.875rem;
margin-top: 0.25rem;
display: none;
}
.error-message.active {
display: block;
}
/* Overlay */
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.overlay.active {
display: block;
}
/* Tag Input Styles */
.tag-input-container {
position: relative;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.5rem;
background: var(--background-color);
min-height: 40px;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.tag-input-container:focus-within {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-color-alpha);
}
.tag-display {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.tag {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: var(--tag-bg);
color: var(--text-color);
border-radius: 16px;
font-size: 0.875rem;
transition: all var(--transition-speed) ease;
}
.tag .remove-tag {
cursor: pointer;
opacity: 0.6;
transition: opacity var(--transition-speed) ease;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--text-color);
color: var(--tag-bg);
}
.tag .remove-tag:hover {
opacity: 1;
}
#promptTags {
flex: 1;
min-width: 100px;
border: none;
outline: none;
background: none;
padding: 0.25rem;
color: var(--text-color);
}
.tag-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: 4px;
margin-top: 0.25rem;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.tag-suggestions.active {
display: block;
}
.tag-suggestion {
padding: 0.5rem;
cursor: pointer;
transition: background var(--transition-speed) ease;
}
.tag-suggestion:hover,
.tag-suggestion.selected {
background: var(--hover-color);
}
.prompt-card .tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.prompt-card .tag {
font-size: 0.75rem;
padding: 0.125rem 0.375rem;
}
/* Popular Tags */
.popular-tags {
margin-top: 1.5rem;
}
.popular-tags h3 {
margin-bottom: 1rem;
color: var(--text-color);
font-size: 1rem;
}
.tags-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tags-cloud .tag {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: var(--tag-bg);
color: var(--text-color);
border-radius: 16px;
font-size: 0.875rem;
cursor: pointer;
transition: all var(--transition-speed) ease;
user-select: none;
}
.tags-cloud .tag.active,
.tags-cloud .tag:hover {
background: var(--primary-color);
color: white;
transform: translateY(-1px);
}
.tags-cloud .tag.active .tag-count,
.tags-cloud .tag:hover .tag-count {
background: white;
color: var(--primary-color);
}
.tags-cloud .tag .tag-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 0.25rem;
background: var(--text-color);
color: var(--tag-bg);
border-radius: 10px;
font-size: 0.75rem;
font-weight: 500;
}
/* Button Styles */
.primary-btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: background-color var(--transition-speed);
}
.secondary-btn {
background-color: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: background-color var(--transition-speed);
}
.icon-btn {
background: none;
border: none;
padding: 4px 8px;
cursor: pointer;
color: #666;
transition: color var(--transition-speed);
}
.icon-btn:hover {
color: var(--primary-color);
}
/* Prompt Card Styles */
.prompt-list {
display: grid;
gap: var(--spacing);
}
.prompt-card {
background-color: var(--card-background);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--spacing);
margin-bottom: var(--spacing);
box-shadow: var(--shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
cursor: pointer;
position: relative;
}
.prompt-card:hover {
transform: translateY(-2px);
box-shadow: var(--form-shadow);
}
.prompt-card.selected {
border: 2px solid var(--primary-color);
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2);
background-color: var(--hover-color);
}
.prompt-card.selected:hover {
transform: translateY(-2px);
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.3);
}
.prompt-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.prompt-title {
margin: 0;
font-size: 1.2em;
color: var(--text-color);
flex: 1;
margin-right: 1rem;
}
.prompt-content {
font-family: "Consolas", "Monaco", "Courier New", monospace;
background-color: var(--hover-color);
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
max-height: 300px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
}
.prompt-content::-webkit-scrollbar {
width: 8px;
}
.prompt-content::-webkit-scrollbar-track {
background: transparent;
}
.prompt-content::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 4px;
border: 2px solid var(--hover-color);
}
.prompt-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: var(--card-background);
border-radius: var(--border-radius);
padding: var(--spacing);
width: 90%;
max-width: 500px;
box-shadow: var(--shadow);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: var(--spacing);
}
/* Toast Styles */
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.toast {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 12px 20px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
justify-content: space-between;
min-width: 300px;
animation: slideIn 0.3s ease-out;
}
.toast-content {
display: flex;
align-items: center;
gap: 10px;
}
.toast-icon {
font-size: 1.2em;
}
.toast.success .toast-icon {
color: var(--secondary-color);
}
.toast.error .toast-icon {
color: var(--danger-color);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Utility Classes */
.hidden {
display: none;
}
/* Responsive Design */
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.sidebar {
margin-bottom: var(--spacing);
}
}
/* Sidebar Sections */
.popular-tags {
margin-bottom: 1.5rem;
}
.tags-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
background: var(--tag-bg);
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
cursor: pointer;
transition: background-color var(--transition-speed);
}
.tag:hover {
background: var(--tag-hover);
}
.sidebar .actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1.5rem;
}
.sidebar .actions button {
width: 100%;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.sidebar .actions button i {
font-size: 0.9rem;
}
/* Bulk Actions Styles */
.bulk-actions {
position: sticky;
top: 80px; /* Position below header */
left: 0;
right: 0;
background-color: var(--card-background);
padding: 15px 20px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
display: none;
gap: 10px;
align-items: center;
z-index: 100;
margin: 0 auto 20px auto;
max-width: 1200px;
border: 1px solid var(--border-color);
}
.bulk-actions.visible {
display: flex;
justify-content: space-between;
}
.bulk-actions-left {
display: flex;
align-items: center;
gap: 15px;
}
.bulk-actions-right {
display: flex;
gap: 10px;
}
.bulk-actions button {
padding: 8px 16px;
border-radius: var(--border-radius);
border: none;
background-color: var(--primary-color);
color: white;
cursor: pointer;
transition: background-color var(--transition-speed) ease;
font-size: 14px;
}
.bulk-actions button:hover {
background-color: var(--secondary-color);
}
.bulk-actions .selected-count {
color: var(--text-color);
font-weight: 500;
}
.bulk-actions .clear-selection {
background-color: transparent;
color: var(--text-color);
border: 1px solid var(--border-color);
}
.bulk-actions .clear-selection:hover {
background-color: var(--hover-color);
}
.bulk-actions .bulk-delete {
background-color: var(--danger-color);
}
.bulk-actions .bulk-delete:hover {
background-color: var(--danger-color);
opacity: 0.9;
}
/* Pagination Styles */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.pagination button {
padding: 0.5rem 1rem;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--card-background);
color: var(--text-color);
cursor: pointer;
transition: all var(--transition-speed) ease;
font-size: 0.9rem;
}
.pagination button:hover:not(:disabled) {
border-color: var(--primary-color);
color: var(--primary-color);
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination .page-info {
color: var(--text-color);
font-size: 0.9rem;
}
/* Form validation styles */
.error {
border-color: var(--error-color) !important;
}
.error-message {
color: var(--error-color);
font-size: 14px;
margin-top: 4px;
margin-bottom: 8px;
}
.form-group {
position: relative;
margin-bottom: 1.5rem;
}
.form-group.has-error input,
.form-group.has-error textarea,
.form-group.has-error select,
.form-group.has-error .tags-input-container {
border-color: var(--danger-color);
}
.error-message {
color: var(--error-color);
font-size: 0.85rem;
margin-top: 0.5rem;
min-height: 1.2em;
opacity: 0;
transform: translateY(-10px);
transition: all var(--transition-speed) ease;
}
.form-group.has-error .error-message {
opacity: 1;
transform: translateY(0);
}
/* Shake animation for invalid inputs */
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.form-group.has-error input:focus,
.form-group.has-error textarea:focus,
.form-group.has-error select:focus,
.form-group.has-error .tags-input-container:focus-within {
border-color: var(--danger-color);
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.2);
}
.form-group.has-error .form-label {
color: var(--danger-color);
}
.bulk-move-modal .category-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 1rem 0;
}
.bulk-move-modal .category-item {
padding: 0.75rem;
border-radius: var(--border-radius);
background: var(--card-background);
border: 2px solid var(--border-color);
cursor: pointer;
transition: all var(--transition-speed) ease;
}
.bulk-move-modal .category-item:hover {
border-color: var(--primary-color);
background: var(--hover-color);
}
.bulk-move-modal .category-item.selected {
border-color: var(--primary-color);
background: var(--primary-color);
color: white;
}
/* Template Styles */
.prompt-input-container {
position: relative;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.template-toolbar {
display: flex;
gap: 0.5rem;
padding: 0.5rem 0;
}
.template-toolbar .icon-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius);
background: var(--background-alt);
color: var(--text-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all var(--transition-speed) ease;
}
.template-toolbar .icon-btn:hover {
background: var(--hover-color);
}
.template-variables {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.template-variable {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--background-alt);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
}
.template-variable input {
width: 150px;
padding: 0.25rem 0.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--background-color);
color: var(--text-color);
}
.template-variable .remove-variable {
color: var(--danger-color);
cursor: pointer;
opacity: 0.7;
transition: opacity var(--transition-speed) ease;
}
.template-variable .remove-variable:hover {
opacity: 1;
}
.preview-content {
white-space: pre-wrap;
padding: 1rem;
background: var(--background-alt);
border-radius: var(--border-radius);
margin: 1rem 0;
}
.preview-variables {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.preview-variable {
padding: 0.5rem;
background: var(--background-color);
border-radius: var(--border-radius);
}
/* Template Form Styles */
.template-form {
padding: 1rem;
max-height: 70vh;
overflow-y: auto;
}
.template-form .form-group {
margin-bottom: 1rem;
}
.template-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: var(--text-color);
}
.template-form textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--input-bg);
color: var(--text-color);
font-family: inherit;
resize: vertical;
}
.template-preview {
margin-top: 1.5rem;
padding: 1rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--card-bg);
}
.template-preview h4 {
margin: 0 0 0.5rem 0;
color: var(--text-color);
}
.preview-content {
margin: 0;
padding: 0.5rem;
background: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
color: var(--text-color);
}
.template-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.template-field label {
font-weight: 600;
color: var(--text-color);
}
.template-field input {
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--background-color);
color: var(--text-color);
}
.template-field input:focus {
border-color: var(--primary-color);
outline: none;
}
.use-template-btn {
color: var(--primary-color);
}
.use-template-btn:hover {
color: var(--primary-color-dark);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment