feat: redesign UI for improved file explorer and workspaces

- Refactor file tree logic into `SimpleFileTree` class
- Implement new explorer with collapse, refresh, search, and selection controls
- Redesign selection, prompt, and chat workspaces with new layouts and styles
- Introduce dedicated CSS icon set for various UI elements
- Add prompt generation and clipboard copy functionality for prompt output
This commit is contained in:
Mahmoud-Emad
2025-08-24 11:34:30 +03:00
parent 03de3a6aee
commit de9e310867
3 changed files with 1499 additions and 616 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,17 @@
console.log('Heroprompt UI loaded'); console.log('Enhanced HeroPrompt UI loaded');
// Global state
let currentWs = localStorage.getItem('heroprompt-current-ws') || 'default'; let currentWs = localStorage.getItem('heroprompt-current-ws') || 'default';
let selected = []; let selected = new Set();
let expandedDirs = new Set();
let searchQuery = '';
// Utility functions
const el = (id) => document.getElementById(id); const el = (id) => document.getElementById(id);
const qs = (selector) => document.querySelector(selector);
const qsa = (selector) => document.querySelectorAll(selector);
// API helpers
async function api(url) { async function api(url) {
try { try {
const r = await fetch(url); const r = await fetch(url);
@@ -13,8 +20,7 @@ async function api(url) {
return { error: `HTTP ${r.status}` }; return { error: `HTTP ${r.status}` };
} }
return await r.json(); return await r.json();
} } catch (e) {
catch (e) {
console.warn(`API call error: ${url}`, e); console.warn(`API call error: ${url}`, e);
return { error: 'request failed' }; return { error: 'request failed' };
} }
@@ -30,14 +36,13 @@ async function post(url, data) {
return { error: `HTTP ${r.status}` }; return { error: `HTTP ${r.status}` };
} }
return await r.json(); return await r.json();
} } catch (e) {
catch (e) {
console.warn(`POST error: ${url}`, e); console.warn(`POST error: ${url}`, e);
return { error: 'request failed' }; return { error: 'request failed' };
} }
} }
// Bootstrap modal helpers // Modal helpers
function showModal(id) { function showModal(id) {
const modalEl = el(id); const modalEl = el(id);
if (modalEl) { if (modalEl) {
@@ -54,277 +59,343 @@ function hideModal(id) {
} }
} }
// Tab switching with Bootstrap // Tab management
function switchTab(tabName) { function switchTab(tabName) {
// Hide all tab panes // Update tab buttons
document.querySelectorAll('.tab-pane').forEach(pane => { qsa('.tab').forEach(tab => {
pane.style.display = 'none';
pane.classList.remove('active');
});
// Remove active class from all tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active'); tab.classList.remove('active');
if (tab.getAttribute('data-tab') === tabName) {
tab.classList.add('active');
}
}); });
// Show selected tab pane // Update tab panes
const targetPane = el(`tab-${tabName}`); qsa('.tab-pane').forEach(pane => {
if (targetPane) { pane.style.display = 'none';
targetPane.style.display = 'block'; if (pane.id === `tab-${tabName}`) {
targetPane.classList.add('active'); pane.style.display = 'block';
} }
});
// Add active class to clicked tab
const targetTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
if (targetTab) {
targetTab.classList.add('active');
}
} }
// Initialize tab switching // Simple and clean file tree implementation
document.addEventListener('DOMContentLoaded', function () { class SimpleFileTree {
document.querySelectorAll('.tab').forEach(tab => { constructor(container) {
tab.addEventListener('click', function (e) { this.container = container;
e.preventDefault(); this.loadedPaths = new Set();
const tabName = this.getAttribute('data-tab'); }
switchTab(tabName);
createFileItem(item, path, depth = 0) {
const div = document.createElement('div');
div.className = 'tree-item';
div.style.paddingLeft = `${depth * 16}px`;
div.dataset.path = path;
div.dataset.type = item.type;
const content = document.createElement('div');
content.className = 'tree-item-content';
// Checkbox
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'tree-checkbox';
checkbox.checked = selected.has(path);
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
selected.add(path);
} else {
selected.delete(path);
}
this.updateSelectionUI();
}); });
});
});
// Checkbox-based collapsible tree // Expand/collapse button for directories
let nodeId = 0; let expandBtn = null;
if (item.type === 'directory') {
expandBtn = document.createElement('button');
expandBtn.className = 'tree-expand-btn';
expandBtn.innerHTML = expandedDirs.has(path) ? '▼' : '▶';
expandBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleDirectory(path, expandBtn);
});
} else {
// Spacer for files to align with directories
expandBtn = document.createElement('span');
expandBtn.className = 'tree-expand-spacer';
}
function renderTree(displayName, fullPath) { // Icon
const c = document.createElement('div'); const icon = document.createElement('span');
c.className = 'tree'; icon.className = 'tree-icon';
const ul = document.createElement('ul'); icon.textContent = item.type === 'directory' ? '📁' : '📄';
ul.className = 'tree-root list-unstyled';
const root = buildDirNode(displayName, fullPath, true);
ul.appendChild(root);
c.appendChild(ul);
return c;
}
function buildDirNode(name, fullPath, expanded = false) { // Label
const li = document.createElement('li'); const label = document.createElement('span');
li.className = 'dir mb-1'; label.className = 'tree-label';
const id = `tn_${nodeId++}`; label.textContent = item.name;
label.addEventListener('click', () => {
const toggle = document.createElement('input'); if (item.type === 'file') {
toggle.type = 'checkbox'; this.previewFile(path);
toggle.className = 'toggle d-none'; } else {
toggle.id = id; this.toggleDirectory(path, expandBtn);
if (expanded) toggle.checked = true;
const label = document.createElement('label');
label.htmlFor = id;
label.className = 'dir-label d-flex align-items-center text-decoration-none';
label.style.cursor = 'pointer';
const icon = document.createElement('span');
icon.className = 'chev me-1';
icon.innerHTML = expanded ? '📂' : '📁';
const text = document.createElement('span');
text.className = 'name flex-grow-1';
text.textContent = name;
label.appendChild(icon);
label.appendChild(text);
const add = document.createElement('button');
add.className = 'btn btn-sm btn-outline-primary ms-1';
add.textContent = '+';
add.title = 'Add directory to selection';
add.onclick = (e) => {
e.stopPropagation();
addDirToSelection(fullPath);
};
const children = document.createElement('ul');
children.className = 'children list-unstyled ms-3';
children.style.display = expanded ? 'block' : 'none';
toggle.addEventListener('change', async () => {
if (toggle.checked) {
children.style.display = 'block';
icon.innerHTML = '📂';
if (!li.dataset.loaded) {
await loadChildren(fullPath, children);
li.dataset.loaded = '1';
} }
});
content.appendChild(checkbox);
content.appendChild(expandBtn);
content.appendChild(icon);
content.appendChild(label);
div.appendChild(content);
return div;
}
async toggleDirectory(dirPath, expandBtn) {
const isExpanded = expandedDirs.has(dirPath);
if (isExpanded) {
// Collapse
expandedDirs.delete(dirPath);
expandBtn.innerHTML = '▶';
this.removeChildren(dirPath);
} else { } else {
children.style.display = 'none'; // Expand
icon.innerHTML = '📁'; expandedDirs.add(dirPath);
} expandBtn.innerHTML = '▼';
}); await this.loadChildren(dirPath);
// Load immediately if expanded by default
if (expanded) {
setTimeout(async () => {
await loadChildren(fullPath, children);
li.dataset.loaded = '1';
}, 0);
}
li.appendChild(toggle);
li.appendChild(label);
li.appendChild(add);
li.appendChild(children);
return li;
}
function createFileNode(name, fullPath) {
const li = document.createElement('li');
li.className = 'file d-flex align-items-center mb-1';
const icon = document.createElement('span');
icon.className = 'me-2';
icon.innerHTML = '📄';
const a = document.createElement('a');
a.href = '#';
a.className = 'text-decoration-none flex-grow-1';
a.textContent = name;
a.onclick = (e) => {
e.preventDefault();
previewFile(fullPath);
};
const add = document.createElement('button');
add.className = 'btn btn-sm btn-outline-primary ms-1';
add.textContent = '+';
add.title = 'Add file to selection';
add.onclick = (e) => {
e.stopPropagation();
addFileToSelection(fullPath);
};
li.appendChild(icon);
li.appendChild(a);
li.appendChild(add);
return li;
}
async function previewFile(filePath) {
const previewEl = el('preview');
if (!previewEl) return;
previewEl.innerHTML = '<div class="loading">Loading...</div>';
const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`);
if (r.error) {
previewEl.innerHTML = `<div class="error-message">Error: ${r.error}</div>`;
return;
}
previewEl.textContent = r.content || 'No content';
}
async function loadChildren(parentPath, ul) {
const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
if (r.error) {
ul.innerHTML = `<li class="text-danger small">${r.error}</li>`;
return;
}
ul.innerHTML = '';
for (const it of r.items || []) {
const full = parentPath.endsWith('/') ? parentPath + it.name : parentPath + '/' + it.name;
if (it.type === 'directory') {
ul.appendChild(buildDirNode(it.name, full, false));
} else {
ul.appendChild(createFileNode(it.name, full));
} }
} }
}
async function loadDir(p) { removeChildren(parentPath) {
const treeEl = el('tree'); const items = qsa('.tree-item');
if (!treeEl) return; items.forEach(item => {
const itemPath = item.dataset.path;
if (itemPath !== parentPath && itemPath.startsWith(parentPath + '/')) {
item.remove();
}
});
}
treeEl.innerHTML = '<div class="loading">Loading workspace...</div>'; async loadChildren(parentPath) {
const display = p.split('/').filter(Boolean).slice(-1)[0] || p; if (this.loadedPaths.has(parentPath)) {
treeEl.appendChild(renderTree(display, p)); return; // Already loaded
updateSelectionList(); }
}
function updateSelectionList() { console.log('Loading children for:', parentPath);
const selCountEl = el('selCount'); const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
const tokenCountEl = el('tokenCount');
const selectedEl = el('selected');
if (selCountEl) selCountEl.textContent = String(selected.length); if (r.error) {
if (selectedEl) { console.warn('Failed to load directory:', parentPath, r.error);
selectedEl.innerHTML = ''; return;
if (selected.length === 0) { }
selectedEl.innerHTML = '<li class="text-muted small">No files selected</li>';
} else {
for (const p of selected) {
const li = document.createElement('li');
li.className = 'd-flex justify-content-between align-items-center mb-1 p-2 border rounded';
const span = document.createElement('span'); console.log('API response for', parentPath, ':', r);
span.className = 'small';
span.textContent = p;
const btn = document.createElement('button'); // Sort items: directories first, then files
btn.className = 'btn btn-sm btn-outline-danger'; const items = (r.items || []).sort((a, b) => {
btn.textContent = '×'; if (a.type !== b.type) {
btn.onclick = () => { return a.type === 'directory' ? -1 : 1;
selected = selected.filter(x => x !== p); }
updateSelectionList(); return a.name.localeCompare(b.name);
}; });
li.appendChild(span); console.log('Sorted items:', items);
li.appendChild(btn);
selectedEl.appendChild(li); // Find the parent element
const parentElement = qs(`[data-path="${parentPath}"]`);
if (!parentElement) {
console.warn('Parent element not found for path:', parentPath);
return;
}
const parentDepth = this.getDepth(parentPath);
console.log('Parent depth:', parentDepth);
// Insert children after parent
let insertAfter = parentElement;
for (const item of items) {
const childPath = parentPath.endsWith('/') ?
parentPath + item.name :
parentPath + '/' + item.name;
console.log('Creating child:', item.name, 'at path:', childPath, 'depth:', parentDepth + 1);
const childElement = this.createFileItem(item, childPath, parentDepth + 1);
insertAfter.insertAdjacentElement('afterend', childElement);
insertAfter = childElement;
}
this.loadedPaths.add(parentPath);
console.log('Finished loading children for:', parentPath);
}
getDepth(path) {
const depth = (path.match(/\//g) || []).length;
console.log('Depth for path', path, ':', depth);
return depth;
}
async previewFile(filePath) {
const previewEl = el('preview');
if (!previewEl) return;
previewEl.innerHTML = '<div class="loading">Loading...</div>';
const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`);
if (r.error) {
previewEl.innerHTML = `<div class="error-message">Error: ${r.error}</div>`;
return;
}
previewEl.textContent = r.content || 'No content';
}
updateSelectionUI() {
const selCountEl = el('selCount');
const selCountTabEl = el('selCountTab');
const tokenCountEl = el('tokenCount');
const selectedEl = el('selected');
const count = selected.size;
if (selCountEl) selCountEl.textContent = count.toString();
if (selCountTabEl) selCountTabEl.textContent = count.toString();
// Update selection list
if (selectedEl) {
selectedEl.innerHTML = '';
if (count === 0) {
selectedEl.innerHTML = `
<li class="empty-selection">
<i class="icon-empty"></i>
<p>No files selected</p>
<small>Use checkboxes in the explorer to select files</small>
</li>
`;
} else {
Array.from(selected).forEach(path => {
const li = document.createElement('li');
li.className = 'selected-item';
const span = document.createElement('span');
span.className = 'item-path';
span.textContent = path;
const btn = document.createElement('button');
btn.className = 'btn btn-xs btn-ghost';
btn.innerHTML = '<i class="icon-close"></i>';
btn.onclick = () => {
this.removeFromSelection(path);
};
li.appendChild(span);
li.appendChild(btn);
selectedEl.appendChild(li);
});
} }
} }
// Estimate token count (rough approximation)
const totalChars = Array.from(selected).join('\n').length;
const tokens = Math.ceil(totalChars / 4);
if (tokenCountEl) tokenCountEl.textContent = tokens.toString();
} }
// naive token estimator ~ 4 chars/token removeFromSelection(path) {
const tokens = Math.ceil(selected.join('\n').length / 4); selected.delete(path);
if (tokenCountEl) tokenCountEl.textContent = String(Math.ceil(tokens));
}
function addToSelection(p) { // Update checkbox
if (!selected.includes(p)) { const checkbox = qs(`[data-path="${path}"] .tree-checkbox`);
selected.push(p); if (checkbox) {
updateSelectionList(); checkbox.checked = false;
}
this.updateSelectionUI();
}
selectAll() {
qsa('.tree-checkbox').forEach(checkbox => {
checkbox.checked = true;
const path = checkbox.closest('.tree-item').dataset.path;
selected.add(path);
});
this.updateSelectionUI();
}
clearSelection() {
selected.clear();
qsa('.tree-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
this.updateSelectionUI();
}
collapseAll() {
expandedDirs.clear();
qsa('.tree-expand-btn').forEach(btn => {
btn.innerHTML = '▶';
});
// Remove all children except root level
qsa('.tree-item').forEach(item => {
const depth = parseInt(item.style.paddingLeft) / 16;
if (depth > 0) {
item.remove();
}
});
this.loadedPaths.clear();
}
search(query) {
searchQuery = query.toLowerCase();
qsa('.tree-item').forEach(item => {
const label = item.querySelector('.tree-label');
if (label) {
const matches = !searchQuery || label.textContent.toLowerCase().includes(searchQuery);
item.style.display = matches ? 'block' : 'none';
}
});
}
async render(workspacePath) {
this.container.innerHTML = '<div class="loading">Loading workspace...</div>';
const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(workspacePath)}`);
this.container.innerHTML = '';
if (r.error) {
this.container.innerHTML = `<div class="error-message">${r.error}</div>`;
return;
}
// Sort items: directories first, then files
const items = (r.items || []).sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'directory' ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
for (const item of items) {
const fullPath = workspacePath.endsWith('/') ?
workspacePath + item.name :
workspacePath + '/' + item.name;
const element = this.createFileItem(item, fullPath, 0);
this.container.appendChild(element);
}
this.updateSelectionUI();
} }
} }
async function addDirToSelection(p) { // Global tree instance
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/dirs`, { let fileTree = null;
method: 'POST',
body: new URLSearchParams({ path: p })
});
const j = await r.json().catch(() => ({ error: 'request failed' }));
if (j && j.ok !== false && !j.error) {
if (!selected.includes(p)) selected.push(p);
updateSelectionList();
} else {
console.warn('Failed to add directory:', j.error || 'Unknown error');
}
}
async function addFileToSelection(p) { // Workspace management
if (selected.includes(p)) return;
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/files`, {
method: 'POST',
body: new URLSearchParams({ path: p })
});
const j = await r.json().catch(() => ({ error: 'request failed' }));
if (j && j.ok !== false && !j.error) {
selected.push(p);
updateSelectionList();
} else {
console.warn('Failed to add file:', j.error || 'Unknown error');
}
}
// Workspaces list + selector
async function reloadWorkspaces() { async function reloadWorkspaces() {
const sel = el('workspaceSelect'); const sel = el('workspaceSelect');
if (!sel) return; if (!sel) return;
@@ -346,7 +417,6 @@ async function reloadWorkspaces() {
sel.appendChild(opt); sel.appendChild(opt);
} }
// ensure current ws name exists or select first
if (names.includes(currentWs)) { if (names.includes(currentWs)) {
sel.value = currentWs; sel.value = currentWs;
} else if (names.length > 0) { } else if (names.length > 0) {
@@ -356,14 +426,19 @@ async function reloadWorkspaces() {
} }
} }
// On initial load: pick current or first workspace and load its base
async function initWorkspace() { async function initWorkspace() {
const names = await api('/api/heroprompt/workspaces'); const names = await api('/api/heroprompt/workspaces');
if (names.error || !Array.isArray(names) || names.length === 0) { if (names.error || !Array.isArray(names) || names.length === 0) {
console.warn('No workspaces available'); console.warn('No workspaces available');
const treeEl = el('tree'); const treeEl = el('tree');
if (treeEl) { if (treeEl) {
treeEl.innerHTML = '<div class="text-muted small">No workspaces available. Create one to get started.</div>'; treeEl.innerHTML = `
<div class="empty-state">
<i class="icon-folder-open"></i>
<p>No workspaces available</p>
<small>Create one to get started</small>
</div>
`;
} }
return; return;
} }
@@ -378,16 +453,85 @@ async function initWorkspace() {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || ''; const base = info?.base_path || '';
if (base) await loadDir(base); if (base && fileTree) {
await fileTree.render(base);
}
}
// Prompt generation
async function generatePrompt() {
const promptText = el('promptText')?.value || '';
const outputEl = el('promptOutput');
if (!outputEl) return;
if (selected.size === 0) {
outputEl.innerHTML = '<div class="error-message">No files selected. Please select files first.</div>';
return;
}
outputEl.innerHTML = '<div class="loading">Generating prompt...</div>';
try {
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/prompt`, {
method: 'POST',
body: new URLSearchParams({ text: promptText })
});
const result = await r.text();
outputEl.textContent = result;
} catch (e) {
console.warn('Generate prompt failed', e);
outputEl.innerHTML = '<div class="error-message">Failed to generate prompt</div>';
}
}
async function copyPrompt() {
const outputEl = el('promptOutput');
if (!outputEl) return;
const text = outputEl.textContent;
if (!text || text.includes('No files selected') || text.includes('Failed')) {
return;
}
try {
await navigator.clipboard.writeText(text);
// Show success feedback
const originalContent = outputEl.innerHTML;
outputEl.innerHTML = '<div class="success-message">Prompt copied to clipboard!</div>';
setTimeout(() => {
outputEl.innerHTML = originalContent;
}, 2000);
} catch (e) {
console.warn('Copy failed', e);
outputEl.innerHTML = '<div class="error-message">Failed to copy prompt</div>';
}
} }
// Initialize everything when DOM is ready // Initialize everything when DOM is ready
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Initialize file tree
const treeContainer = el('tree');
if (treeContainer) {
fileTree = new SimpleFileTree(treeContainer);
}
// Initialize workspaces // Initialize workspaces
initWorkspace(); initWorkspace();
reloadWorkspaces(); reloadWorkspaces();
// Workspace selector change handler // Tab switching
qsa('.tab').forEach(tab => {
tab.addEventListener('click', function (e) {
e.preventDefault();
const tabName = this.getAttribute('data-tab');
switchTab(tabName);
});
});
// Workspace selector
const workspaceSelect = el('workspaceSelect'); const workspaceSelect = el('workspaceSelect');
if (workspaceSelect) { if (workspaceSelect) {
workspaceSelect.addEventListener('change', async (e) => { workspaceSelect.addEventListener('change', async (e) => {
@@ -395,11 +539,87 @@ document.addEventListener('DOMContentLoaded', function () {
localStorage.setItem('heroprompt-current-ws', currentWs); localStorage.setItem('heroprompt-current-ws', currentWs);
const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || ''; const base = info?.base_path || '';
if (base) await loadDir(base); if (base && fileTree) {
await fileTree.render(base);
}
}); });
} }
// Create workspace modal handlers // Explorer controls
const collapseAllBtn = el('collapseAll');
if (collapseAllBtn) {
collapseAllBtn.addEventListener('click', () => {
if (fileTree) fileTree.collapseAll();
});
}
const refreshExplorerBtn = el('refreshExplorer');
if (refreshExplorerBtn) {
refreshExplorerBtn.addEventListener('click', async () => {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base && fileTree) {
await fileTree.render(base);
}
});
}
const selectAllBtn = el('selectAll');
if (selectAllBtn) {
selectAllBtn.addEventListener('click', () => {
if (fileTree) fileTree.selectAll();
});
}
const clearSelectionBtn = el('clearSelection');
if (clearSelectionBtn) {
clearSelectionBtn.addEventListener('click', () => {
if (fileTree) fileTree.clearSelection();
});
}
const clearAllSelectionBtn = el('clearAllSelection');
if (clearAllSelectionBtn) {
clearAllSelectionBtn.addEventListener('click', () => {
if (fileTree) fileTree.clearSelection();
});
}
// Search functionality
const searchInput = el('search');
const clearSearchBtn = el('clearSearch');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
if (fileTree) {
fileTree.search(e.target.value);
}
});
}
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', () => {
if (searchInput) {
searchInput.value = '';
if (fileTree) {
fileTree.search('');
}
}
});
}
// Prompt generation
const generatePromptBtn = el('generatePrompt');
if (generatePromptBtn) {
generatePromptBtn.addEventListener('click', generatePrompt);
}
const copyPromptBtn = el('copyPrompt');
if (copyPromptBtn) {
copyPromptBtn.addEventListener('click', copyPrompt);
}
// Workspace creation modal
const wsCreateBtn = el('wsCreateBtn'); const wsCreateBtn = el('wsCreateBtn');
if (wsCreateBtn) { if (wsCreateBtn) {
wsCreateBtn.addEventListener('click', () => { wsCreateBtn.addEventListener('click', () => {
@@ -442,72 +662,15 @@ document.addEventListener('DOMContentLoaded', function () {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || ''; const base = info?.base_path || '';
if (base) await loadDir(base); if (base && fileTree) {
await fileTree.render(base);
}
hideModal('wsCreate'); hideModal('wsCreate');
}); });
} }
// Refresh workspace handler // Workspace details modal
const refreshBtn = el('refreshWs');
if (refreshBtn) {
refreshBtn.addEventListener('click', async () => {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
});
}
// Search handler
const searchBtn = el('doSearch');
if (searchBtn) {
searchBtn.onclick = async () => {
const q = el('search')?.value?.trim();
if (!q) return;
// For now, just show a message since search endpoint might not exist
const tree = el('tree');
if (tree) {
tree.innerHTML = '<div class="text-muted small">Search functionality coming soon...</div>';
}
};
}
// Copy prompt handler
const copyPromptBtn = el('copyPrompt');
if (copyPromptBtn) {
copyPromptBtn.addEventListener('click', async () => {
const text = el('promptText')?.value || '';
try {
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/prompt`, {
method: 'POST',
body: new URLSearchParams({ text })
});
const out = await r.text();
await navigator.clipboard.writeText(out);
// Show success feedback
const outputEl = el('promptOutput');
if (outputEl) {
outputEl.innerHTML = '<div class="success-message">Prompt copied to clipboard!</div>';
setTimeout(() => {
outputEl.innerHTML = '<div class="text-muted small">Generated prompt will appear here</div>';
}, 3000);
}
} catch (e) {
console.warn('copy prompt failed', e);
const outputEl = el('promptOutput');
if (outputEl) {
outputEl.innerHTML = '<div class="error-message">Failed to copy prompt</div>';
setTimeout(() => {
outputEl.innerHTML = '<div class="text-muted small">Generated prompt will appear here</div>';
}, 3000);
}
}
});
}
// Workspace details modal handler
const wsDetailsBtn = el('wsDetailsBtn'); const wsDetailsBtn = el('wsDetailsBtn');
if (wsDetailsBtn) { if (wsDetailsBtn) {
wsDetailsBtn.addEventListener('click', async () => { wsDetailsBtn.addEventListener('click', async () => {
@@ -526,7 +689,7 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
} }
// Workspace manage modal handler // Workspace management modal
const openWsManageBtn = el('openWsManage'); const openWsManageBtn = el('openWsManage');
if (openWsManageBtn) { if (openWsManageBtn) {
openWsManageBtn.addEventListener('click', async () => { openWsManageBtn.addEventListener('click', async () => {
@@ -535,7 +698,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (!list) return; if (!list) return;
if (err) err.textContent = ''; if (err) err.textContent = '';
list.innerHTML = '<div class="text-muted">Loading workspaces...</div>'; list.innerHTML = '<div class="loading">Loading workspaces...</div>';
const names = await api('/api/heroprompt/workspaces'); const names = await api('/api/heroprompt/workspaces');
list.innerHTML = ''; list.innerHTML = '';
@@ -561,7 +724,9 @@ document.addEventListener('DOMContentLoaded', function () {
await reloadWorkspaces(); await reloadWorkspaces();
const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || ''; const base = info?.base_path || '';
if (base) await loadDir(base); if (base && fileTree) {
await fileTree.render(base);
}
hideModal('wsManage'); hideModal('wsManage');
}; };
@@ -573,4 +738,6 @@ document.addEventListener('DOMContentLoaded', function () {
showModal('wsManage'); showModal('wsManage');
}); });
} }
console.log('Enhanced HeroPrompt UI initialized');
}); });

View File

@@ -45,122 +45,262 @@
</div> </div>
<div class="row h-100"> <div class="row h-100">
<!-- Left Panel: Workspace & File Tree --> <!-- Left Panel: Enhanced File Explorer -->
<div class="col-md-4 h-100"> <div class="col-md-4 h-100">
<div class="card h-100"> <div class="explorer-panel h-100">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="explorer-header">
<h6 class="mb-0">Workspace Explorer</h6> <div class="d-flex justify-content-between align-items-center mb-2">
<div> <h6 class="mb-0 text-uppercase fw-bold explorer-title">Explorer</h6>
<button id="wsDetailsBtn" class="btn btn-sm btn-outline-secondary me-1" <div class="explorer-actions">
title="Workspace Settings"></button> <button id="collapseAll" class="btn btn-sm btn-ghost me-1" title="Collapse All">
<button id="openWsManage" class="btn btn-sm btn-outline-secondary" <i class="icon-collapse"></i>
title="Manage Workspaces">📋</button> </button>
</div> <button id="refreshExplorer" class="btn btn-sm btn-ghost me-1" title="Refresh">
</div> <i class="icon-refresh"></i>
<div class="card-body p-2"> </button>
<div class="mb-3"> <button id="wsDetailsBtn" class="btn btn-sm btn-ghost me-1"
<label for="workspaceSelect" class="form-label small">Current Workspace:</label> title="Workspace Settings">
<select id="workspaceSelect" class="form-select form-select-sm"></select> <i class="icon-settings"></i>
</div> </button>
<button id="openWsManage" class="btn btn-sm btn-ghost" title="Manage Workspaces">
<div class="mb-3"> <i class="icon-manage"></i>
<div class="input-group input-group-sm"> </button>
<input id="search" class="form-control" placeholder="Search files...">
<button id="doSearch" class="btn btn-outline-secondary">🔍</button>
</div> </div>
</div> </div>
<div id="tree" class="border rounded p-2" <div class="workspace-selector mb-3">
style="min-height: 300px; max-height: 400px; overflow-y: auto;"> <select id="workspaceSelect" class="form-select form-select-sm modern-select"></select>
<div class="text-muted small">Select a workspace to browse files</div>
</div> </div>
<div class="mt-2"> <div class="search-container mb-3">
<small class="text-muted">Click + buttons to add files/directories to selection</small> <div class="input-group input-group-sm">
<span class="input-group-text search-icon">
<i class="icon-search"></i>
</span>
<input id="search" class="form-control modern-input"
placeholder="Search files and folders...">
<button id="clearSearch" class="btn btn-ghost search-clear" title="Clear search">
<i class="icon-close"></i>
</button>
</div>
</div>
</div>
<div class="explorer-content">
<div class="selection-controls mb-2">
<div class="d-flex justify-content-between align-items-center">
<div class="selection-info">
<span class="badge badge-selection">
<span id="selCount">0</span> selected
</span>
</div>
<div class="selection-actions">
<button id="selectAll" class="btn btn-xs btn-ghost" title="Select All Visible">
Select All
</button>
<button id="clearSelection" class="btn btn-xs btn-ghost"
title="Clear Selection">
Clear
</button>
</div>
</div>
</div>
<div id="tree" class="file-tree">
<div class="empty-state">
<i class="icon-folder-open"></i>
<p>Select a workspace to browse files</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Right Panel: Tabs for Selection, Prompt, Chat --> <!-- Right Panel: Enhanced Workspace -->
<div class="col-md-8 h-100"> <div class="col-md-8 h-100">
<div class="card h-100"> <div class="workspace-panel h-100">
<div class="card-header"> <div class="workspace-header">
<ul class="nav nav-tabs card-header-tabs" id="mainTabs"> <div class="tab-navigation">
<li class="nav-item"> <div class="nav nav-tabs modern-tabs" id="mainTabs">
<a class="nav-link active tab" data-tab="selection" href="#tab-selection"> <button class="nav-link active tab" data-tab="selection">
Selection (<span id="selCount">0</span>) <i class="icon-selection"></i>
</a> <span>Selection</span>
</li> <span class="badge badge-count" id="selCountTab">0</span>
<li class="nav-item"> </button>
<a class="nav-link tab" data-tab="prompt" href="#tab-prompt">Prompt</a> <button class="nav-link tab" data-tab="prompt">
</li> <i class="icon-prompt"></i>
<li class="nav-item"> <span>Prompt</span>
<a class="nav-link tab" data-tab="chat" href="#tab-chat">Chat</a> </button>
</li> <button class="nav-link tab" data-tab="chat">
</ul> <i class="icon-chat"></i>
</div> <span>Chat</span>
<div class="card-body p-0 h-100"> </button>
<!-- Selection Tab -->
<div id="tab-selection" class="tab-pane active h-100 p-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Selected Files & Directories</h6>
<span class="badge bg-secondary">~<span id="tokenCount">0</span> tokens</span>
</div> </div>
<div class="row h-100"> <div class="workspace-actions">
<div class="col-md-6"> <span class="token-counter">
<div class="border rounded p-2" style="height: 300px; overflow-y: auto;"> <i class="icon-token"></i>
<ul id="selected" class="list-unstyled mb-0"> <span id="tokenCount">0</span> tokens
<li class="text-muted small">No files selected</li> </span>
</ul> </div>
</div>
</div>
<div class="workspace-content">
<!-- Selection Tab -->
<div id="tab-selection" class="tab-pane active">
<div class="selection-workspace">
<div class="selection-header">
<h6 class="section-title">Selected Files & Directories</h6>
<div class="selection-actions">
<button id="exportSelection" class="btn btn-sm btn-ghost"
title="Export Selection">
<i class="icon-export"></i>
</button>
<button id="importSelection" class="btn btn-sm btn-ghost"
title="Import Selection">
<i class="icon-import"></i>
</button>
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="border rounded p-2" style="height: 300px; overflow-y: auto;"> <div class="selection-content">
<div id="preview" class="text-muted small">Select a file to preview</div> <div class="selection-list-panel">
<div class="panel-header">
<span class="panel-title">Selected Items</span>
<button id="clearAllSelection" class="btn btn-xs btn-ghost">Clear
All</button>
</div>
<div class="selection-list">
<ul id="selected" class="selected-items">
<li class="empty-selection">
<i class="icon-empty"></i>
<p>No files selected</p>
<small>Use checkboxes in the explorer to select files</small>
</li>
</ul>
</div>
</div>
<div class="preview-panel">
<div class="panel-header">
<span class="panel-title">File Preview</span>
<div class="preview-actions">
<button id="copyPreview" class="btn btn-xs btn-ghost"
title="Copy Content">
<i class="icon-copy"></i>
</button>
</div>
</div>
<div class="preview-content">
<div id="preview" class="file-preview">
<div class="empty-preview">
<i class="icon-file"></i>
<p>Select a file to preview</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Prompt Tab --> <!-- Prompt Tab -->
<div id="tab-prompt" class="tab-pane h-100 p-3" style="display: none;"> <div id="tab-prompt" class="tab-pane" style="display: none;">
<div class="h-100 d-flex flex-column"> <div class="prompt-workspace">
<div class="mb-3"> <div class="prompt-editor">
<label for="promptText" class="form-label">Instructions:</label> <div class="editor-header">
<textarea id="promptText" class="form-control" rows="8" placeholder="Enter your instructions for what needs to be done with the selected code... <h6 class="section-title">Prompt Instructions</h6>
<div class="editor-actions">
<button id="loadTemplate" class="btn btn-sm btn-ghost"
title="Load Template">
<i class="icon-template"></i>
</button>
<button id="saveTemplate" class="btn btn-sm btn-ghost"
title="Save Template">
<i class="icon-save"></i>
</button>
</div>
</div>
<div class="editor-content">
<textarea id="promptText" class="modern-textarea" rows="8" placeholder="Enter your instructions for what needs to be done with the selected code...
Example: Example:
- Analyze the code structure - Analyze the code structure and identify potential improvements
- Identify potential improvements - Add comprehensive error handling and validation
- Add error handling - Optimize performance and reduce complexity
- Optimize performance - Add detailed documentation and comments
- Add documentation"></textarea> - Implement best practices and design patterns"></textarea>
</div>
<div class="editor-footer">
<button id="generatePrompt" class="btn btn-primary">
<i class="icon-generate"></i>
Generate Prompt
</button>
<button id="copyPrompt" class="btn btn-secondary">
<i class="icon-copy"></i>
Copy to Clipboard
</button>
</div>
</div> </div>
<div class="mb-3">
<button id="copyPrompt" class="btn btn-primary">Copy Generated Prompt</button> <div class="prompt-output">
</div> <div class="output-header">
<div class="flex-grow-1"> <span class="panel-title">Generated Prompt</span>
<div id="promptOutput" class="border rounded p-2 h-100 overflow-auto"> <div class="output-actions">
<div class="text-muted small">Generated prompt will appear here</div> <button id="copyOutput" class="btn btn-xs btn-ghost"
title="Copy Output">
<i class="icon-copy"></i>
</button>
</div>
</div>
<div class="output-content">
<div id="promptOutput" class="prompt-result">
<div class="empty-output">
<i class="icon-prompt"></i>
<p>Generated prompt will appear here</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Chat Tab --> <!-- Chat Tab -->
<div id="tab-chat" class="tab-pane h-100 p-3" style="display: none;"> <div id="tab-chat" class="tab-pane" style="display: none;">
<div class="h-100 d-flex flex-column"> <div class="chat-workspace">
<div class="flex-grow-1 border rounded p-2 mb-3 overflow-auto" <div class="chat-header">
style="height: 300px;"> <h6 class="section-title">AI Assistant</h6>
<div id="chatMessages"> <div class="chat-actions">
<div class="text-muted small">Chat functionality coming soon...</div> <button id="clearChat" class="btn btn-sm btn-ghost" title="Clear Chat">
<i class="icon-clear"></i>
</button>
<button id="exportChat" class="btn btn-sm btn-ghost" title="Export Chat">
<i class="icon-export"></i>
</button>
</div> </div>
</div> </div>
<div class="input-group">
<textarea id="chatInput" class="form-control" rows="2" <div class="chat-content">
placeholder="Type your message..."></textarea> <div class="chat-messages">
<button id="sendChat" class="btn btn-primary">Send</button> <div id="chatMessages" class="messages-container">
<div class="welcome-message">
<i class="icon-ai"></i>
<p>AI Assistant ready to help!</p>
<small>Ask questions about your selected code or get
suggestions</small>
</div>
</div>
</div>
<div class="chat-input">
<div class="input-container">
<textarea id="chatInput" class="chat-textarea" rows="2"
placeholder="Ask about your code, request explanations, or get suggestions..."></textarea>
<button id="sendChat" class="btn btn-primary send-btn">
<i class="icon-send"></i>
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>