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:
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user