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:
267
lib/web/ui/templates/css/heroprompt.css
Normal file
267
lib/web/ui/templates/css/heroprompt.css
Normal 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: "📁";
|
||||
}
|
||||
129
lib/web/ui/templates/heroprompt.html
Normal file
129
lib/web/ui/templates/heroprompt.html
Normal 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>
|
||||
610
lib/web/ui/templates/js/heroprompt.js
Normal file
610
lib/web/ui/templates/js/heroprompt.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user