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 selected = [];
let selected = new Set();
let expandedDirs = new Set();
let searchQuery = '';
// Utility functions
const el = (id) => document.getElementById(id);
const qs = (selector) => document.querySelector(selector);
const qsa = (selector) => document.querySelectorAll(selector);
// API helpers
async function api(url) {
try {
const r = await fetch(url);
@@ -13,8 +20,7 @@ async function api(url) {
return { error: `HTTP ${r.status}` };
}
return await r.json();
}
catch (e) {
} catch (e) {
console.warn(`API call error: ${url}`, e);
return { error: 'request failed' };
}
@@ -30,14 +36,13 @@ async function post(url, data) {
return { error: `HTTP ${r.status}` };
}
return await r.json();
}
catch (e) {
} catch (e) {
console.warn(`POST error: ${url}`, e);
return { error: 'request failed' };
}
}
// Bootstrap modal helpers
// Modal helpers
function showModal(id) {
const modalEl = el(id);
if (modalEl) {
@@ -54,277 +59,343 @@ function hideModal(id) {
}
}
// Tab switching with Bootstrap
// Tab management
function switchTab(tabName) {
// Hide all tab panes
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.style.display = 'none';
pane.classList.remove('active');
});
// Remove active class from all tabs
document.querySelectorAll('.tab').forEach(tab => {
// Update tab buttons
qsa('.tab').forEach(tab => {
tab.classList.remove('active');
if (tab.getAttribute('data-tab') === tabName) {
tab.classList.add('active');
}
});
// Show selected tab pane
const targetPane = el(`tab-${tabName}`);
if (targetPane) {
targetPane.style.display = 'block';
targetPane.classList.add('active');
}
// Add active class to clicked tab
const targetTab = document.querySelector(`.tab[data-tab="${tabName}"]`);
if (targetTab) {
targetTab.classList.add('active');
}
// Update tab panes
qsa('.tab-pane').forEach(pane => {
pane.style.display = 'none';
if (pane.id === `tab-${tabName}`) {
pane.style.display = 'block';
}
});
}
// Initialize tab switching
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function (e) {
e.preventDefault();
const tabName = this.getAttribute('data-tab');
switchTab(tabName);
// Simple and clean file tree implementation
class SimpleFileTree {
constructor(container) {
this.container = container;
this.loadedPaths = new Set();
}
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
let nodeId = 0;
// Expand/collapse button for directories
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) {
const c = document.createElement('div');
c.className = 'tree';
const ul = document.createElement('ul');
ul.className = 'tree-root list-unstyled';
const root = buildDirNode(displayName, fullPath, true);
ul.appendChild(root);
c.appendChild(ul);
return c;
}
// Icon
const icon = document.createElement('span');
icon.className = 'tree-icon';
icon.textContent = item.type === 'directory' ? '📁' : '📄';
function buildDirNode(name, fullPath, expanded = false) {
const li = document.createElement('li');
li.className = 'dir mb-1';
const id = `tn_${nodeId++}`;
const toggle = document.createElement('input');
toggle.type = 'checkbox';
toggle.className = 'toggle d-none';
toggle.id = id;
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';
// Label
const label = document.createElement('span');
label.className = 'tree-label';
label.textContent = item.name;
label.addEventListener('click', () => {
if (item.type === 'file') {
this.previewFile(path);
} else {
this.toggleDirectory(path, expandBtn);
}
});
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 {
children.style.display = 'none';
icon.innerHTML = '📁';
}
});
// 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));
// Expand
expandedDirs.add(dirPath);
expandBtn.innerHTML = '▼';
await this.loadChildren(dirPath);
}
}
}
async function loadDir(p) {
const treeEl = el('tree');
if (!treeEl) return;
removeChildren(parentPath) {
const items = qsa('.tree-item');
items.forEach(item => {
const itemPath = item.dataset.path;
if (itemPath !== parentPath && itemPath.startsWith(parentPath + '/')) {
item.remove();
}
});
}
treeEl.innerHTML = '<div class="loading">Loading workspace...</div>';
const display = p.split('/').filter(Boolean).slice(-1)[0] || p;
treeEl.appendChild(renderTree(display, p));
updateSelectionList();
}
async loadChildren(parentPath) {
if (this.loadedPaths.has(parentPath)) {
return; // Already loaded
}
function updateSelectionList() {
const selCountEl = el('selCount');
const tokenCountEl = el('tokenCount');
const selectedEl = el('selected');
console.log('Loading children for:', parentPath);
const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
if (selCountEl) selCountEl.textContent = String(selected.length);
if (selectedEl) {
selectedEl.innerHTML = '';
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';
if (r.error) {
console.warn('Failed to load directory:', parentPath, r.error);
return;
}
const span = document.createElement('span');
span.className = 'small';
span.textContent = p;
console.log('API response for', parentPath, ':', r);
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-outline-danger';
btn.textContent = '×';
btn.onclick = () => {
selected = selected.filter(x => x !== p);
updateSelectionList();
};
// 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);
});
li.appendChild(span);
li.appendChild(btn);
selectedEl.appendChild(li);
console.log('Sorted items:', items);
// 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
const tokens = Math.ceil(selected.join('\n').length / 4);
if (tokenCountEl) tokenCountEl.textContent = String(Math.ceil(tokens));
}
removeFromSelection(path) {
selected.delete(path);
function addToSelection(p) {
if (!selected.includes(p)) {
selected.push(p);
updateSelectionList();
// Update checkbox
const checkbox = qs(`[data-path="${path}"] .tree-checkbox`);
if (checkbox) {
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) {
const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/dirs`, {
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');
}
}
// Global tree instance
let fileTree = null;
async function addFileToSelection(p) {
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
// Workspace management
async function reloadWorkspaces() {
const sel = el('workspaceSelect');
if (!sel) return;
@@ -346,7 +417,6 @@ async function reloadWorkspaces() {
sel.appendChild(opt);
}
// ensure current ws name exists or select first
if (names.includes(currentWs)) {
sel.value = currentWs;
} 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() {
const names = await api('/api/heroprompt/workspaces');
if (names.error || !Array.isArray(names) || names.length === 0) {
console.warn('No workspaces available');
const treeEl = el('tree');
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;
}
@@ -378,16 +453,85 @@ async function initWorkspace() {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
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
document.addEventListener('DOMContentLoaded', function () {
// Initialize file tree
const treeContainer = el('tree');
if (treeContainer) {
fileTree = new SimpleFileTree(treeContainer);
}
// Initialize workspaces
initWorkspace();
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');
if (workspaceSelect) {
workspaceSelect.addEventListener('change', async (e) => {
@@ -395,11 +539,87 @@ document.addEventListener('DOMContentLoaded', function () {
localStorage.setItem('heroprompt-current-ws', currentWs);
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
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');
if (wsCreateBtn) {
wsCreateBtn.addEventListener('click', () => {
@@ -442,72 +662,15 @@ document.addEventListener('DOMContentLoaded', function () {
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
if (base && fileTree) {
await fileTree.render(base);
}
hideModal('wsCreate');
});
}
// Refresh workspace handler
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
// Workspace details modal
const wsDetailsBtn = el('wsDetailsBtn');
if (wsDetailsBtn) {
wsDetailsBtn.addEventListener('click', async () => {
@@ -526,7 +689,7 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
// Workspace manage modal handler
// Workspace management modal
const openWsManageBtn = el('openWsManage');
if (openWsManageBtn) {
openWsManageBtn.addEventListener('click', async () => {
@@ -535,7 +698,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (!list) return;
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');
list.innerHTML = '';
@@ -561,7 +724,9 @@ document.addEventListener('DOMContentLoaded', function () {
await reloadWorkspaces();
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
const base = info?.base_path || '';
if (base) await loadDir(base);
if (base && fileTree) {
await fileTree.render(base);
}
hideModal('wsManage');
};
@@ -573,4 +738,6 @@ document.addEventListener('DOMContentLoaded', function () {
showModal('wsManage');
});
}
console.log('Enhanced HeroPrompt UI initialized');
});

View File

@@ -45,122 +45,262 @@
</div>
<div class="row h-100">
<!-- Left Panel: Workspace & File Tree -->
<!-- Left Panel: Enhanced File Explorer -->
<div class="col-md-4 h-100">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Workspace Explorer</h6>
<div>
<button id="wsDetailsBtn" class="btn btn-sm btn-outline-secondary me-1"
title="Workspace Settings"></button>
<button id="openWsManage" class="btn btn-sm btn-outline-secondary"
title="Manage Workspaces">📋</button>
</div>
</div>
<div class="card-body p-2">
<div class="mb-3">
<label for="workspaceSelect" class="form-label small">Current Workspace:</label>
<select id="workspaceSelect" class="form-select form-select-sm"></select>
</div>
<div class="mb-3">
<div class="input-group input-group-sm">
<input id="search" class="form-control" placeholder="Search files...">
<button id="doSearch" class="btn btn-outline-secondary">🔍</button>
<div class="explorer-panel h-100">
<div class="explorer-header">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0 text-uppercase fw-bold explorer-title">Explorer</h6>
<div class="explorer-actions">
<button id="collapseAll" class="btn btn-sm btn-ghost me-1" title="Collapse All">
<i class="icon-collapse"></i>
</button>
<button id="refreshExplorer" class="btn btn-sm btn-ghost me-1" title="Refresh">
<i class="icon-refresh"></i>
</button>
<button id="wsDetailsBtn" class="btn btn-sm btn-ghost me-1"
title="Workspace Settings">
<i class="icon-settings"></i>
</button>
<button id="openWsManage" class="btn btn-sm btn-ghost" title="Manage Workspaces">
<i class="icon-manage"></i>
</button>
</div>
</div>
<div id="tree" class="border rounded p-2"
style="min-height: 300px; max-height: 400px; overflow-y: auto;">
<div class="text-muted small">Select a workspace to browse files</div>
<div class="workspace-selector mb-3">
<select id="workspaceSelect" class="form-select form-select-sm modern-select"></select>
</div>
<div class="mt-2">
<small class="text-muted">Click + buttons to add files/directories to selection</small>
<div class="search-container mb-3">
<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>
<!-- Right Panel: Tabs for Selection, Prompt, Chat -->
<!-- Right Panel: Enhanced Workspace -->
<div class="col-md-8 h-100">
<div class="card h-100">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="mainTabs">
<li class="nav-item">
<a class="nav-link active tab" data-tab="selection" href="#tab-selection">
Selection (<span id="selCount">0</span>)
</a>
</li>
<li class="nav-item">
<a class="nav-link tab" data-tab="prompt" href="#tab-prompt">Prompt</a>
</li>
<li class="nav-item">
<a class="nav-link tab" data-tab="chat" href="#tab-chat">Chat</a>
</li>
</ul>
</div>
<div class="card-body p-0 h-100">
<!-- 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 class="workspace-panel h-100">
<div class="workspace-header">
<div class="tab-navigation">
<div class="nav nav-tabs modern-tabs" id="mainTabs">
<button class="nav-link active tab" data-tab="selection">
<i class="icon-selection"></i>
<span>Selection</span>
<span class="badge badge-count" id="selCountTab">0</span>
</button>
<button class="nav-link tab" data-tab="prompt">
<i class="icon-prompt"></i>
<span>Prompt</span>
</button>
<button class="nav-link tab" data-tab="chat">
<i class="icon-chat"></i>
<span>Chat</span>
</button>
</div>
<div class="row h-100">
<div class="col-md-6">
<div class="border rounded p-2" style="height: 300px; overflow-y: auto;">
<ul id="selected" class="list-unstyled mb-0">
<li class="text-muted small">No files selected</li>
</ul>
<div class="workspace-actions">
<span class="token-counter">
<i class="icon-token"></i>
<span id="tokenCount">0</span> tokens
</span>
</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 class="col-md-6">
<div class="border rounded p-2" style="height: 300px; overflow-y: auto;">
<div id="preview" class="text-muted small">Select a file to preview</div>
<div class="selection-content">
<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>
<!-- Prompt Tab -->
<div id="tab-prompt" class="tab-pane h-100 p-3" style="display: none;">
<div class="h-100 d-flex flex-column">
<div class="mb-3">
<label for="promptText" class="form-label">Instructions:</label>
<textarea id="promptText" class="form-control" rows="8" placeholder="Enter your instructions for what needs to be done with the selected code...
<div id="tab-prompt" class="tab-pane" style="display: none;">
<div class="prompt-workspace">
<div class="prompt-editor">
<div class="editor-header">
<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:
- Analyze the code structure
- Identify potential improvements
- Add error handling
- Optimize performance
- Add documentation"></textarea>
- Analyze the code structure and identify potential improvements
- Add comprehensive error handling and validation
- Optimize performance and reduce complexity
- Add detailed documentation and comments
- 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 class="mb-3">
<button id="copyPrompt" class="btn btn-primary">Copy Generated Prompt</button>
</div>
<div class="flex-grow-1">
<div id="promptOutput" class="border rounded p-2 h-100 overflow-auto">
<div class="text-muted small">Generated prompt will appear here</div>
<div class="prompt-output">
<div class="output-header">
<span class="panel-title">Generated Prompt</span>
<div class="output-actions">
<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>
<!-- Chat Tab -->
<div id="tab-chat" class="tab-pane h-100 p-3" style="display: none;">
<div class="h-100 d-flex flex-column">
<div class="flex-grow-1 border rounded p-2 mb-3 overflow-auto"
style="height: 300px;">
<div id="chatMessages">
<div class="text-muted small">Chat functionality coming soon...</div>
<div id="tab-chat" class="tab-pane" style="display: none;">
<div class="chat-workspace">
<div class="chat-header">
<h6 class="section-title">AI Assistant</h6>
<div class="chat-actions">
<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 class="input-group">
<textarea id="chatInput" class="form-control" rows="2"
placeholder="Type your message..."></textarea>
<button id="sendChat" class="btn btn-primary">Send</button>
<div class="chat-content">
<div class="chat-messages">
<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>