feat: add Heroprompt workspace management UI

- Add HTML, CSS, and JS for the Heroprompt feature
- Implement a three-panel UI for workspaces and files
- Add logic for creating/deleting workspaces in localStorage
- Enable adding directories and selecting files for
This commit is contained in:
Mahmoud-Emad
2025-08-13 15:37:36 +03:00
parent 5c77c6bd8d
commit a6d4a23172
3 changed files with 1006 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
/* Heroprompt-specific styles */
.heroprompt-container {
display: flex;
gap: 1rem;
height: calc(100vh - 200px);
}
.workspaces-panel {
flex: 0 0 280px;
min-width: 280px;
}
.workspace-details {
flex: 1;
min-width: 0;
}
.instructions-panel {
flex: 0 0 350px;
min-width: 350px;
}
/* Workspace list styling */
.workspace-item {
cursor: pointer;
transition: background-color 0.2s ease;
border: none !important;
padding: 0.75rem 1rem;
}
.workspace-item:hover {
background-color: var(--bs-gray-100);
}
[data-bs-theme="dark"] .workspace-item:hover {
background-color: var(--bs-gray-800);
}
.workspace-item.active {
background-color: var(--bs-primary);
color: white;
}
.workspace-item.active:hover {
background-color: var(--bs-primary);
}
/* Directory and file styling */
.directory-item {
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
margin-bottom: 1rem;
background-color: var(--bs-body-bg);
}
.directory-header {
background-color: var(--bs-gray-50);
border-bottom: 1px solid var(--bs-border-color);
padding: 0.75rem 1rem;
font-weight: 600;
border-radius: 0.375rem 0.375rem 0 0;
}
[data-bs-theme="dark"] .directory-header {
background-color: var(--bs-gray-800);
}
.file-list {
padding: 0.5rem;
max-height: 300px;
overflow-y: auto;
}
.file-item {
display: flex;
align-items: center;
padding: 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.2s ease;
margin-bottom: 0.25rem;
}
.file-item:hover {
background-color: var(--bs-gray-100);
}
[data-bs-theme="dark"] .file-item:hover {
background-color: var(--bs-gray-700);
}
.file-item.selected {
background-color: var(--bs-success-bg-subtle);
border: 1px solid var(--bs-success-border-subtle);
}
.file-item input[type="checkbox"] {
margin-right: 0.5rem;
}
.file-name {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
}
/* Empty state styling */
.empty-state {
text-align: center;
padding: 2rem;
color: var(--bs-text-muted);
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Button styling */
.btn-group-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Toast styling */
.toast {
min-width: 300px;
}
/* Instructions panel styling */
.instructions-panel textarea {
resize: vertical;
min-height: 200px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
}
.instructions-panel .card-body {
padding: 1rem;
}
/* Responsive design */
@media (max-width: 1200px) {
.heroprompt-container {
flex-direction: column;
height: auto;
}
.workspaces-panel,
.workspace-details,
.instructions-panel {
flex: none;
min-width: auto;
margin-bottom: 1rem;
}
.instructions-panel {
order: 3;
}
}
@media (max-width: 768px) {
.heroprompt-container {
flex-direction: column;
height: auto;
}
.workspaces-panel,
.workspace-details,
.instructions-panel {
flex: none;
min-width: auto;
margin-bottom: 1rem;
}
}
/* Selection counter */
.selection-counter {
font-size: 0.875rem;
color: var(--bs-text-muted);
margin-left: 0.5rem;
}
.selection-counter.has-selection {
color: var(--bs-success);
font-weight: 600;
}
/* Directory path styling */
.directory-path {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.75rem;
color: var(--bs-text-muted);
margin-top: 0.25rem;
}
/* Workspace name input */
.workspace-name-input {
border: 1px solid var(--bs-border-color);
border-radius: 0.25rem;
padding: 0.5rem;
width: 100%;
margin-bottom: 0.5rem;
}
/* Loading state */
.loading-spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid var(--bs-border-color);
border-radius: 50%;
border-top-color: var(--bs-primary);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* File type icons */
.file-icon {
margin-right: 0.5rem;
width: 16px;
text-align: center;
}
.file-icon.file-md::before {
content: "📝";
}
.file-icon.file-v::before {
content: "⚡";
}
.file-icon.file-js::before {
content: "📜";
}
.file-icon.file-css::before {
content: "🎨";
}
.file-icon.file-html::before {
content: "🌐";
}
.file-icon.file-json::before {
content: "📋";
}
.file-icon.file-yaml::before {
content: "⚙️";
}
.file-icon.file-txt::before {
content: "📄";
}
.file-icon.file-default::before {
content: "📁";
}

View File

@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.title}} - Heroprompt</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="{{.css_colors_url}}">
<link rel="stylesheet" href="{{.css_main_url}}">
<link rel="stylesheet" href="{{.css_heroprompt_url}}">
<meta name="color-scheme" content="light dark">
</head>
<body>
<nav class="navbar navbar-dark bg-dark fixed-top header px-2">
<div class="d-flex w-100 align-items-center justify-content-between">
<div class="text-white fw-bold">{{.title}}</div>
<div class="text-white-50">Heroprompt</div>
</div>
</nav>
<aside class="sidebar">
<div class="p-2">
<div class="menu-section">Navigation</div>
<div class="list-group list-group-flush">
{{.menu_html}}
</div>
</div>
</aside>
<main class="main">
<div class="container-fluid">
<div class="d-flex align-items-center mb-3">
<h5 class="mb-0">Heroprompt</h5>
<span class="ms-2 text-muted small">/admin/heroprompt</span>
</div>
<div class="heroprompt-container">
<!-- Left Column: Workspaces -->
<div class="workspaces-panel">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Workspaces</h6>
<button id="create-workspace" class="btn btn-sm btn-primary">New</button>
</div>
<div class="card-body p-0">
<div id="workspace-list" class="list-group list-group-flush">
<!-- Workspaces will be populated by JavaScript -->
</div>
</div>
</div>
</div>
<!-- Middle Column: Workspace Details -->
<div class="workspace-details">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<span id="current-workspace-name">Select a workspace</span>
<button id="delete-workspace" class="btn btn-sm btn-outline-danger ms-2"
style="display: none;">Delete</button>
</h6>
<div>
<button id="add-directory" class="btn btn-sm btn-secondary me-2"
style="display: none;">Add Directory</button>
<button id="copy-selection" class="btn btn-sm btn-success" style="display: none;">Copy
Selection</button>
</div>
</div>
<div class="card-body">
<div id="workspace-content">
<p class="text-muted">Select a workspace to view its directories and files.</p>
</div>
</div>
</div>
</div>
<!-- Right Column: User Instructions -->
<div class="instructions-panel">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">User Instructions</h6>
<button id="clear-instructions" class="btn btn-sm btn-outline-secondary">Clear</button>
</div>
<div class="card-body">
<textarea id="user-instructions" class="form-control" rows="10" placeholder="Enter your instructions for what needs to be done with the selected code...
Example:
- Analyze the code structure
- Identify potential improvements
- Add error handling
- Optimize performance
- Add documentation"></textarea>
<div class="mt-3">
<small class="text-muted">
These instructions will be included in the generated prompt along with the selected
files.
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Toast for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="notification-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">Heroprompt</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
<!-- Toast message will be set by JavaScript -->
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script src="{{.js_theme_url}}"></script>
<script src="{{.js_heroprompt_url}}"></script>
</body>
</html>

View File

@@ -0,0 +1,610 @@
/**
* Heroprompt - Client-side workspace and file selection management
* Updated to work with V backend API and support subdirectories
*/
class Heroprompt {
constructor() {
this.storageKey = 'heroprompt_data';
this.data = this.loadData();
this.currentWorkspace = this.data.current || 'default';
// Ensure default workspace exists
if (!this.data.workspaces.default) {
this.data.workspaces.default = { dirs: [] };
this.saveData();
}
this.initializeUI();
this.bindEvents();
this.render();
}
// Data management
loadData() {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.warn('Failed to load heroprompt data:', e);
}
return {
workspaces: {
default: { dirs: [] }
},
current: 'default'
};
}
saveData() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
} catch (e) {
console.error('Failed to save heroprompt data:', e);
this.showToast('Failed to save data', 'error');
}
}
// API calls to V backend
async fetchDirectory(path) {
try {
const response = await fetch(`/api/heroprompt/directory?path=${encodeURIComponent(path)}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (e) {
console.error('Failed to fetch directory:', e);
throw e;
}
}
async fetchFileContent(path) {
try {
const response = await fetch(`/api/heroprompt/file?path=${encodeURIComponent(path)}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (e) {
console.error('Failed to fetch file:', e);
throw e;
}
}
// Workspace management
createWorkspace(name) {
if (!name || name.trim() === '') {
this.showToast('Workspace name cannot be empty', 'error');
return;
}
name = name.trim();
if (this.data.workspaces[name]) {
this.showToast('Workspace already exists', 'error');
return;
}
this.data.workspaces[name] = { dirs: [] };
this.data.current = name;
this.currentWorkspace = name;
this.saveData();
this.render();
this.showToast(`Workspace "${name}" created`, 'success');
}
deleteWorkspace(name) {
if (name === 'default') {
this.showToast('Cannot delete default workspace', 'error');
return;
}
if (!confirm(`Are you sure you want to delete workspace "${name}"?`)) {
return;
}
delete this.data.workspaces[name];
if (this.currentWorkspace === name) {
this.currentWorkspace = 'default';
this.data.current = 'default';
}
this.saveData();
this.render();
this.showToast(`Workspace "${name}" deleted`, 'success');
}
selectWorkspace(name) {
if (!this.data.workspaces[name]) {
this.showToast('Workspace not found', 'error');
return;
}
this.currentWorkspace = name;
this.data.current = name;
this.saveData();
this.render();
}
// Directory management
async addDirectory() {
// Try to use the File System Access API if available
if ('showDirectoryPicker' in window) {
await this.addDirectoryWithPicker();
} else {
this.addDirectoryWithPrompt();
}
}
async addDirectoryWithPicker() {
try {
const dirHandle = await window.showDirectoryPicker();
// For File System Access API, we need to read the directory contents directly
// since we can't pass the handle to the backend
const files = [];
const subdirs = [];
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
files.push(name);
} else if (handle.kind === 'directory') {
subdirs.push(name);
}
}
const processedDir = {
path: dirHandle.name, // Use the directory name as path for now
files: files.sort(),
subdirs: subdirs.sort(),
selected: []
};
this.data.workspaces[this.currentWorkspace].dirs.push(processedDir);
this.saveData();
this.render();
this.showToast(`Directory "${dirHandle.name}" added`, 'success');
} catch (e) {
if (e.name !== 'AbortError') {
console.error('Directory picker error:', e);
this.showToast('Failed to access directory', 'error');
}
}
}
async processDirectoryHandle(dirHandle, basePath = '') {
// This method is no longer used with the File System Access API approach
// but kept for compatibility
try {
const fullPath = basePath ? `${basePath}/${dirHandle.name}` : dirHandle.name;
const dirData = await this.fetchDirectory(fullPath);
// Check if we got an error response
if (dirData.error) {
throw new Error(dirData.error);
}
// Process the directory structure from the backend
const processedDir = {
path: dirData.path,
files: [],
subdirs: [],
selected: []
};
// Separate files and directories
if (dirData.items && Array.isArray(dirData.items)) {
for (const item of dirData.items) {
if (item.type === 'file') {
processedDir.files.push(item.name);
} else if (item.type === 'directory') {
processedDir.subdirs.push(item.name);
}
}
}
this.data.workspaces[this.currentWorkspace].dirs.push(processedDir);
this.saveData();
this.render();
this.showToast(`Directory "${dirHandle.name}" added`, 'success');
} catch (e) {
console.error('Failed to process directory:', e);
this.showToast('Failed to process directory', 'error');
}
}
addDirectoryWithPrompt() {
const path = prompt('Enter directory path:');
if (!path || path.trim() === '') {
return;
}
this.fetchDirectory(path.trim())
.then(dirData => {
// Check if we got an error response
if (dirData.error) {
throw new Error(dirData.error);
}
const processedDir = {
path: dirData.path || path.trim(),
files: [],
subdirs: [],
selected: []
};
// Separate files and directories
if (dirData.items && Array.isArray(dirData.items)) {
for (const item of dirData.items) {
if (item.type === 'file') {
processedDir.files.push(item.name);
} else if (item.type === 'directory') {
processedDir.subdirs.push(item.name);
}
}
}
this.data.workspaces[this.currentWorkspace].dirs.push(processedDir);
this.saveData();
this.render();
this.showToast(`Directory "${path}" added`, 'success');
})
.catch(e => {
console.error('Failed to add directory:', e);
this.showToast(`Failed to add directory: ${e.message}`, 'error');
});
}
removeDirectory(workspaceName, dirIndex) {
if (!confirm('Are you sure you want to remove this directory?')) {
return;
}
this.data.workspaces[workspaceName].dirs.splice(dirIndex, 1);
this.saveData();
this.render();
this.showToast('Directory removed', 'success');
}
// File selection management
toggleFileSelection(workspaceName, dirIndex, fileName) {
const dir = this.data.workspaces[workspaceName].dirs[dirIndex];
const selectedIndex = dir.selected.indexOf(fileName);
if (selectedIndex === -1) {
dir.selected.push(fileName);
} else {
dir.selected.splice(selectedIndex, 1);
}
this.saveData();
this.renderWorkspaceDetails();
}
selectAllFiles(workspaceName, dirIndex) {
const dir = this.data.workspaces[workspaceName].dirs[dirIndex];
dir.selected = [...dir.files];
this.saveData();
this.renderWorkspaceDetails();
}
deselectAllFiles(workspaceName, dirIndex) {
const dir = this.data.workspaces[workspaceName].dirs[dirIndex];
dir.selected = [];
this.saveData();
this.renderWorkspaceDetails();
}
// Generate file tree structure
generateFileTree(dirPath, files, subdirs) {
const lines = [];
const dirName = dirPath.split('/').pop() || dirPath;
lines.push(`${dirPath}`);
// Add files
files.forEach((file, index) => {
const isLast = index === files.length - 1 && subdirs.length === 0;
lines.push(`${isLast ? '└──' : '├──'} ${file}`);
});
// Add subdirectories (placeholder for now)
subdirs.forEach((subdir, index) => {
const isLast = index === subdirs.length - 1;
lines.push(`${isLast ? '└──' : '├──'} ${subdir}/`);
});
return lines.join('\n');
}
// Clipboard functionality with new format
async copySelection() {
const workspace = this.data.workspaces[this.currentWorkspace];
if (!workspace || workspace.dirs.length === 0) {
this.showToast('No directories in workspace', 'error');
return;
}
const userInstructions = document.getElementById('user-instructions').value.trim();
if (!userInstructions) {
this.showToast('Please enter user instructions', 'error');
return;
}
let hasSelection = false;
const output = [];
// Add user instructions
output.push('<user_instructions>');
output.push(userInstructions);
output.push('</user_instructions>');
output.push('');
// Generate file map
output.push('<file_map>');
for (const dir of workspace.dirs) {
if (dir.selected.length > 0) {
hasSelection = true;
const fileTree = this.generateFileTree(dir.path, dir.files, dir.subdirs || []);
output.push(fileTree);
}
}
output.push('</file_map>');
output.push('');
if (!hasSelection) {
this.showToast('No files selected', 'error');
return;
}
// Generate file contents
output.push('<file_contents>');
try {
for (const dir of workspace.dirs) {
for (const fileName of dir.selected) {
const filePath = `${dir.path}/${fileName}`;
try {
const fileData = await this.fetchFileContent(filePath);
output.push(`File: ${filePath}`);
output.push('');
output.push(`\`\`\`${fileData.language}`);
output.push(fileData.content);
output.push('```');
output.push('');
} catch (e) {
console.error(`Failed to fetch file ${filePath}:`, e);
output.push(`File: ${filePath}`);
output.push('');
output.push('```text');
output.push(`Error: Failed to read file - ${e.message}`);
output.push('```');
output.push('');
}
}
}
} catch (e) {
console.error('Error fetching file contents:', e);
this.showToast('Error fetching file contents', 'error');
return;
}
output.push('</file_contents>');
const text = output.join('\n');
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(text);
this.showToast('Selection copied to clipboard', 'success');
} catch (e) {
console.error('Clipboard error:', e);
this.fallbackCopyToClipboard(text);
}
} else {
this.fallbackCopyToClipboard(text);
}
}
fallbackCopyToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
this.showToast('Selection copied to clipboard', 'success');
} catch (e) {
console.error('Fallback copy failed:', e);
this.showToast('Failed to copy to clipboard', 'error');
}
document.body.removeChild(textArea);
}
// UI Management
initializeUI() {
// Cache DOM elements
this.elements = {
workspaceList: document.getElementById('workspace-list'),
workspaceContent: document.getElementById('workspace-content'),
currentWorkspaceName: document.getElementById('current-workspace-name'),
createWorkspaceBtn: document.getElementById('create-workspace'),
deleteWorkspaceBtn: document.getElementById('delete-workspace'),
addDirectoryBtn: document.getElementById('add-directory'),
copySelectionBtn: document.getElementById('copy-selection'),
clearInstructionsBtn: document.getElementById('clear-instructions'),
userInstructions: document.getElementById('user-instructions'),
toast: document.getElementById('notification-toast')
};
}
bindEvents() {
// Workspace management
this.elements.createWorkspaceBtn.addEventListener('click', () => {
const name = prompt('Enter workspace name:');
if (name) {
this.createWorkspace(name);
}
});
this.elements.deleteWorkspaceBtn.addEventListener('click', () => {
this.deleteWorkspace(this.currentWorkspace);
});
// Directory management
this.elements.addDirectoryBtn.addEventListener('click', () => {
this.addDirectory();
});
// Copy selection
this.elements.copySelectionBtn.addEventListener('click', () => {
this.copySelection();
});
// Clear instructions
this.elements.clearInstructionsBtn.addEventListener('click', () => {
this.elements.userInstructions.value = '';
});
}
render() {
this.renderWorkspaceList();
this.renderWorkspaceDetails();
}
renderWorkspaceList() {
const workspaceNames = Object.keys(this.data.workspaces).sort();
this.elements.workspaceList.innerHTML = workspaceNames.map(name => `
<div class="list-group-item workspace-item ${name === this.currentWorkspace ? 'active' : ''}"
data-workspace="${name}">
<div class="d-flex justify-content-between align-items-center">
<span>${name}</span>
<small class="text-muted">${this.data.workspaces[name].dirs.length} dirs</small>
</div>
</div>
`).join('');
// Bind workspace selection events
this.elements.workspaceList.querySelectorAll('.workspace-item').forEach(item => {
item.addEventListener('click', () => {
const workspaceName = item.dataset.workspace;
this.selectWorkspace(workspaceName);
});
});
}
renderWorkspaceDetails() {
const workspace = this.data.workspaces[this.currentWorkspace];
// Update header
this.elements.currentWorkspaceName.textContent = this.currentWorkspace;
// Show/hide buttons based on selection
const hasWorkspace = !!workspace;
this.elements.deleteWorkspaceBtn.style.display = hasWorkspace && this.currentWorkspace !== 'default' ? 'inline-block' : 'none';
this.elements.addDirectoryBtn.style.display = hasWorkspace ? 'inline-block' : 'none';
this.elements.copySelectionBtn.style.display = hasWorkspace ? 'inline-block' : 'none';
if (!workspace) {
this.elements.workspaceContent.innerHTML = '<p class="text-muted">Select a workspace to view its directories and files.</p>';
return;
}
if (workspace.dirs.length === 0) {
this.elements.workspaceContent.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📁</div>
<p>No directories added to this workspace.</p>
<p class="text-muted">Click "Add Directory" to get started.</p>
</div>
`;
return;
}
// Render directories and files
this.elements.workspaceContent.innerHTML = workspace.dirs.map((dir, dirIndex) => `
<div class="directory-item">
<div class="directory-header">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>${this.getDirectoryName(dir.path)}</strong>
<div class="directory-path">${dir.path}</div>
${dir.subdirs && dir.subdirs.length > 0 ? `<div class="text-muted small">Subdirs: ${dir.subdirs.join(', ')}</div>` : ''}
</div>
<div class="btn-group-actions">
<span class="selection-counter ${dir.selected.length > 0 ? 'has-selection' : ''}">
${dir.selected.length}/${dir.files.length} selected
</span>
<button class="btn btn-sm btn-outline-secondary" onclick="heroprompt.selectAllFiles('${this.currentWorkspace}', ${dirIndex})">All</button>
<button class="btn btn-sm btn-outline-secondary" onclick="heroprompt.deselectAllFiles('${this.currentWorkspace}', ${dirIndex})">None</button>
<button class="btn btn-sm btn-outline-danger" onclick="heroprompt.removeDirectory('${this.currentWorkspace}', ${dirIndex})">Remove</button>
</div>
</div>
</div>
<div class="file-list">
${dir.files.map(file => `
<div class="file-item ${dir.selected.includes(file) ? 'selected' : ''}"
onclick="heroprompt.toggleFileSelection('${this.currentWorkspace}', ${dirIndex}, '${file}')">
<input type="checkbox" ${dir.selected.includes(file) ? 'checked' : ''}
onclick="event.stopPropagation()">
<span class="file-icon ${this.getFileIconClass(file)}"></span>
<span class="file-name">${file}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
}
getDirectoryName(path) {
return path.split('/').pop() || path.split('\\').pop() || path;
}
getFileIconClass(fileName) {
const ext = fileName.split('.').pop().toLowerCase();
return `file-${ext}` || 'file-default';
}
showToast(message, type = 'info') {
const toast = this.elements.toast;
const toastBody = toast.querySelector('.toast-body');
toastBody.textContent = message;
// Set toast color based on type
toast.className = 'toast';
if (type === 'success') {
toast.classList.add('text-bg-success');
} else if (type === 'error') {
toast.classList.add('text-bg-danger');
} else {
toast.classList.add('text-bg-info');
}
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.heroprompt = new Heroprompt();
});
// Export for potential external use
if (typeof module !== 'undefined' && module.exports) {
module.exports = Heroprompt;
}