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 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');
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user