A Pen by Dinh Cuong VU on CodePen.
Created
November 14, 2024 15:54
-
-
Save dinh/39dc41d8da78ee517dcd0b3226011d51 to your computer and use it in GitHub Desktop.
Prompt Manager - Windsurf - V4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
: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