From cf187d46b30c9cea55500f1da79aa8ae595d83af Mon Sep 17 00:00:00 2001
From: Mahmoud-Emad
Date: Thu, 21 Aug 2025 20:01:43 +0300
Subject: [PATCH] feat: improve Heroprompt UI and refactor modules
- Refactor all UI rendering logic into a single `ui` module
- Centralize static assets serving to `/static` directory
- Redesign Heroprompt page with Bootstrap 5 components
- Enhance workspace management and file tree interactions
- Add Bootstrap modal support for UI dialogs
---
lib/web/heroprompt/static/css/main.css | 624 --------------
lib/web/heroprompt/static/js/main.js | 391 ---------
.../ui/{chat/endpoints.v => chat_endpoints.v} | 11 +-
lib/web/ui/{chat/utils.v => chat_utils.v} | 3 +-
.../ui/heroprompt/static/css/heroprompt.css | 267 ------
lib/web/ui/heroprompt/static/js/heroprompt.js | 760 ------------------
.../ui/heroprompt/templates/heroprompt.html | 130 ---
.../endpoints.v => heroprompt_api.v} | 31 +-
.../{heroprompt/page.v => heroprompt_page.v} | 4 +-
.../utils.v => heroprompt_utils.v} | 0
.../endpoints.v => heroscript_endpoints.v} | 11 +-
.../utils.v => heroscript_utils.v} | 3 +-
lib/web/ui/server.v | 36 +-
lib/web/ui/{chat => }/static/css/chat.css | 0
lib/web/ui/static/css/heroprompt.css | 329 ++++++++
.../static/css/heroscript.css | 0
lib/web/ui/{chat => }/static/js/chat.js | 0
lib/web/ui/static/js/heroprompt.js | 576 +++++++++++++
.../{heroscript => }/static/js/heroscript.js | 0
lib/web/ui/{chat => }/templates/chat.html | 0
lib/web/ui/templates/heroprompt.html | 254 ++++++
.../templates/heroscript_editor.html | 0
22 files changed, 1214 insertions(+), 2216 deletions(-)
delete mode 100644 lib/web/heroprompt/static/css/main.css
delete mode 100644 lib/web/heroprompt/static/js/main.js
rename lib/web/ui/{chat/endpoints.v => chat_endpoints.v} (67%)
rename lib/web/ui/{chat/utils.v => chat_utils.v} (77%)
delete mode 100644 lib/web/ui/heroprompt/static/css/heroprompt.css
delete mode 100644 lib/web/ui/heroprompt/static/js/heroprompt.js
delete mode 100644 lib/web/ui/heroprompt/templates/heroprompt.html
rename lib/web/ui/{heroprompt/endpoints.v => heroprompt_api.v} (76%)
rename lib/web/ui/{heroprompt/page.v => heroprompt_page.v} (80%)
rename lib/web/ui/{heroprompt/utils.v => heroprompt_utils.v} (100%)
rename lib/web/ui/{heroscript/endpoints.v => heroscript_endpoints.v} (66%)
rename lib/web/ui/{heroscript/utils.v => heroscript_utils.v} (72%)
rename lib/web/ui/{chat => }/static/css/chat.css (100%)
create mode 100644 lib/web/ui/static/css/heroprompt.css
rename lib/web/ui/{heroscript => }/static/css/heroscript.css (100%)
rename lib/web/ui/{chat => }/static/js/chat.js (100%)
create mode 100644 lib/web/ui/static/js/heroprompt.js
rename lib/web/ui/{heroscript => }/static/js/heroscript.js (100%)
rename lib/web/ui/{chat => }/templates/chat.html (100%)
create mode 100644 lib/web/ui/templates/heroprompt.html
rename lib/web/ui/{heroscript => }/templates/heroscript_editor.html (100%)
diff --git a/lib/web/heroprompt/static/css/main.css b/lib/web/heroprompt/static/css/main.css
deleted file mode 100644
index 309e68c9..00000000
--- a/lib/web/heroprompt/static/css/main.css
+++ /dev/null
@@ -1,624 +0,0 @@
-:root {
- --bg: #111827;
- --text: #e5e7eb;
- --panel: #0b1220;
- --border: #1f2937;
- --muted: #94a3b8;
- --accent: #93c5fd;
- --input: #0f172a;
- --input-border: #334155;
- --btn: #334155;
- --btn-border: #475569;
-}
-
-:root.light {
- --bg: #f9fafb;
- --text: #0f172a;
- --panel: #ffffff;
- --border: #e5e7eb;
- --muted: #475569;
- --accent: #2563eb;
- --input: #ffffff;
- --input-border: #cbd5e1;
- --btn: #e5e7eb;
- --btn-border: #cbd5e1;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
- margin: 0;
- padding: 24px;
- background: var(--bg);
- color: var(--text);
-}
-
-h1 {
- font-size: 20px;
- margin: 0 0 12px
-}
-
-.container {
- max-width: 1200px;
- margin: 0 auto
-}
-
-a {
- color: var(--accent)
-}
-
-.toolbar {
- display: flex;
- gap: 8px;
- align-items: center;
- margin-bottom: 12px
-}
-
-.toolbar .spacer {
- flex: 1
-}
-
-.toolbar input {
- background: var(--input);
- border: 1px solid var(--input-border);
- color: var(--text);
- padding: 6px 8px;
- border-radius: 6px
-}
-
-.toolbar button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer
-}
-
-.layout {
- display: grid;
- grid-template-columns: minmax(300px, 380px) 1fr;
- gap: 16px;
- min-height: 70vh
-}
-
-.sidebar {
- background: var(--panel);
- border: 1px solid var(--border);
- padding: 8px;
- border-radius: 8px
-}
-
-.sidebar-header {
- display: flex;
- gap: 8px;
- row-gap: 6px;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 8px;
- flex-wrap: wrap
-}
-
-.sidebar-header .spacer {
- display: none
-}
-
-.sidebar-header select {
- min-width: 140px;
- max-width: 100%
-}
-
-@media (min-width: 900px) {
- .sidebar-header {
- flex-wrap: nowrap
- }
-
- .sidebar-header .spacer {
- display: block;
- flex: 1
- }
-}
-
-.ws-info {
- font-size: 12px;
- color: var(--muted);
- margin-left: auto
-}
-
-.sidebar-header label {
- font-size: 12px;
- color: var(--muted)
-}
-
-.sidebar-header select {
- background: var(--input);
- border: 1px solid var(--input-border);
- color: var(--text);
- padding: 6px 8px;
- border-radius: 6px
-}
-
-.sidebar-header button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer;
- white-space: nowrap;
- flex: 0 0 auto
-}
-
-.sidebar-body {
- .sidebar-header #wsCreateBtn {
- width: 32px;
- height: 32px;
- padding: 0;
- line-height: 30px;
- text-align: center;
- font-weight: bold
- }
-
- .hint {
- color: var(--muted);
- font-size: 12px
- }
-
- .prompt .actions {
- margin-top: 8px
- }
-
- .prompt .actions button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer
- }
-
- display: flex;
- flex-direction: column;
- gap: 8px
-}
-
-.searchbar {
- display: flex;
- gap: 8px
-}
-
-.searchbar input {
- background: var(--input);
- border: 1px solid var(--input-border);
- color: var(--text);
- padding: 6px 8px;
- border-radius: 6px
-}
-
-.searchbar button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer
-}
-
-.sidebar-header button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer
-}
-
-.main {
- display: flex;
- flex-direction: column;
-
- .prompt {
- background: var(--panel);
- border: 1px solid var(--border);
- padding: 12px;
- border-radius: 8px
- }
-
- .prompt textarea {
- width: 100%;
- min-height: 120px;
- background: var(--input);
- color: var(--text);
- border: 1px solid var(--input-border);
- border-radius: 6px;
- padding: 8px;
- resize: vertical
- }
-
- gap: 16px
-}
-
-/* Tabs */
-.tabs {
- display: flex;
- justify-content: center;
- gap: 8px;
- margin-bottom: 8px
-}
-
-.tab {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer
-}
-
-.tab.active {
- background: transparent;
- border-color: var(--accent);
- color: var(--accent)
-}
-
-.tab-pane {
- display: none;
-}
-
-.tab-pane.active {
- display: block;
-}
-
-.subbar {
- display: flex;
- gap: 8px;
- align-items: center;
- margin: 8px 0
-}
-
-.subbar input {
- background: var(--input);
- border: 1px solid var(--input-border);
- color: var(--text);
- padding: 6px 8px;
- border-radius: 6px
-}
-
-.subbar button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer
-}
-
-.subbar input {
- background: var(--input);
- border: 1px solid var(--input-border);
- color: var(--text);
- padding: 6px 8px;
- border-radius: 6px
-}
-
-.subbar button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer
-}
-
-
-/* Chat */
-.chat {
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- height: 60vh
-}
-
-.messages {
- flex: 1;
- padding: 12px;
- overflow: auto;
- display: flex;
- flex-direction: column;
- gap: 10px
-}
-
-.message {
- display: flex;
- gap: 8px;
-}
-
-.message .bubble {
- max-width: 70%;
- padding: 10px 12px;
- border-radius: 12px;
- border: 1px solid var(--border)
-}
-
-.message.user .bubble {
- background: rgba(99, 102, 241, 0.15);
-}
-
-.message.ai .bubble {
- background: rgba(16, 185, 129, 0.15);
-}
-
-.chat-input {
- border-top: 1px solid var(--border);
- padding: 10px;
- display: flex;
- gap: 8px
-}
-
-.chat-input textarea {
- flex: 1;
- background: var(--input);
- border: 1px solid var(--input-border);
- color: var(--text);
- border-radius: 8px;
- padding: 8px;
- min-height: 60px
-}
-
-.chat-input button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- border-radius: 8px;
- padding: 8px 12px;
- cursor: pointer
-}
-
-.preview {
- background: var(--panel);
- border: 1px solid var(--border);
- padding: 8px;
- border-radius: 6px;
- min-height: 300px
-}
-
-.prompt,
-.selection {
- background: var(--panel);
- border: 1px solid var(--border);
- padding: 12px;
- border-radius: 6px
-}
-
-.prompt textarea {
- width: 100%;
- min-height: 120px;
- background: var(--input);
- color: var(--text);
- border: 1px solid var(--input-border);
- border-radius: 6px;
- padding: 8px;
- resize: vertical;
-}
-
-.prompt .actions {
- margin-top: 8px;
-}
-
-.prompt-output {
- margin-top: 10px;
-}
-
-.prompt-output pre {
- white-space: pre-wrap;
- background: var(--panel);
- border: 1px solid var(--border);
- padding: 8px;
- border-radius: 6px;
-}
-
-/* Collapsible tree styles */
-#tree ul {
- list-style: none;
- margin: 0;
- padding-left: 12px;
-}
-
-#tree li {
- margin: 2px 0;
-}
-
-#tree .dir {
- color: var(--text);
-}
-
-#tree .dir .dir-label .name {
- color: var(--text);
-}
-
-#tree .file a {
- color: var(--text);
- text-decoration: none;
-}
-
-#tree .file a:hover {
- text-decoration: underline;
-}
-
-/* Modal */
-.modal[aria-hidden="true"] {
- display: none
-}
-
-.modal {
- position: fixed;
- inset: 0;
- z-index: 1000;
-}
-
-.modal-backdrop {
- position: absolute;
- inset: 0;
-
- background: rgba(0, 0, 0, 0.5)
-}
-
-.modal .row {
- display: flex;
- gap: 12px
-}
-
-.modal .col {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 8px
-}
-
-.modal .list {
- list-style: none;
- margin: 0;
- padding: 0;
- max-height: 40vh;
- overflow: auto
-}
-
-.modal .list li {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 8px;
- padding: 6px 0;
- border-bottom: 1px dashed var(--border)
-}
-
-.modal .list .use {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- border-radius: 6px;
- padding: 4px 8px;
- cursor: pointer
-}
-
-.modal-dialog {
- position: relative;
- margin: 8vh auto;
- max-width: 520px;
- background: var(--panel);
- color: var(--text);
- border: 1px solid var(--border);
- border-radius: 10px;
- padding: 12px;
- z-index: 1
-}
-
-.modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px
-}
-
-.modal-header .icon {
- background: transparent;
- border: none;
- color: var(--text);
- cursor: pointer;
- font-size: 18px
-}
-
-.modal-body {
- display: flex;
- flex-direction: column;
- gap: 8px
-}
-
-.modal-body input {
- background: var(--input);
- color: var(--text);
- border: 1px solid var(--input-border);
- border-radius: 6px;
- padding: 8px
-}
-
-.modal-footer {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- margin-top: 12px
-}
-
-.modal-footer button {
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- border-radius: 6px;
- padding: 6px 10px;
- cursor: pointer
-}
-
-.error {
- color: #ef4444;
- font-size: 12px;
- min-height: 16px
-}
-
-
-#tree .toggle {
- appearance: none;
- -webkit-appearance: none;
- width: 0;
- height: 0;
- position: absolute;
-}
-
-#tree .dir-label {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- cursor: pointer;
- user-select: none;
-}
-
-#tree .dir .chev {
- display: inline-block;
- transform: rotate(-90deg);
- transition: transform 0.15s ease;
- width: 10px;
- height: 10px;
- border-right: 2px solid #93c5fd;
- border-bottom: 2px solid #93c5fd;
- margin-left: 2px;
-}
-
-#tree .dir .toggle:checked+.dir-label .chev {
- transform: rotate(0deg);
-}
-
-#tree .children {
- display: none;
- margin-left: 16px;
-}
-
-#tree .dir .toggle:checked~.children {
- display: block;
-}
-
-#tree button {
- margin-left: 6px;
- background: var(--btn);
- border: 1px solid var(--btn-border);
- color: var(--text);
- padding: 2px 6px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
-}
-
-#selected li {
- display: flex;
- justify-content: space-between;
- gap: 8px
-}
\ No newline at end of file
diff --git a/lib/web/heroprompt/static/js/main.js b/lib/web/heroprompt/static/js/main.js
deleted file mode 100644
index 40669cd0..00000000
--- a/lib/web/heroprompt/static/js/main.js
+++ /dev/null
@@ -1,391 +0,0 @@
-console.log('Heroprompt UI loaded');
-
-let currentWs = localStorage.getItem('heroprompt-current-ws') || 'default';
-let selected = [];
-
-const el = (id) => document.getElementById(id);
-
-async function api(url) {
- try { const r = await fetch(url); return await r.json(); }
- catch { return { error: 'request failed' }; }
-}
-async function post(url, data) {
- const form = new FormData();
- Object.entries(data).forEach(([k, v]) => form.append(k, v));
- try { const r = await fetch(url, { method: 'POST', body: form }); return await r.json(); }
- catch { return { error: 'request failed' }; }
-}
-
-// Checkbox-based collapsible tree
-let nodeId = 0;
-
-function renderTree(displayName, fullPath) {
- const c = document.createElement('div');
- c.className = 'tree';
- const ul = document.createElement('ul');
- ul.className = 'tree-root';
- const root = buildDirNode(displayName, fullPath, true);
- ul.appendChild(root);
- c.appendChild(ul);
- return c;
-}
-
-function buildDirNode(name, fullPath, expanded = false) {
- const li = document.createElement('li');
- li.className = 'dir';
- const id = `tn_${nodeId++}`;
-
- const toggle = document.createElement('input');
- toggle.type = 'checkbox';
- toggle.className = 'toggle';
- toggle.id = id;
- if (expanded) toggle.checked = true;
-
- const label = document.createElement('label');
- label.htmlFor = id;
- label.className = 'dir-label';
- const icon = document.createElement('span');
- icon.className = 'chev';
- const text = document.createElement('span');
- text.className = 'name';
- text.textContent = name;
- label.appendChild(icon);
- label.appendChild(text);
-
- const add = document.createElement('button');
- add.textContent = '+';
- add.title = 'Add directory to selection';
- add.onclick = () => addDirToSelection(fullPath);
-
- const children = document.createElement('ul');
- children.className = 'children';
-
- toggle.addEventListener('change', async () => {
- if (toggle.checked) {
- if (!li.dataset.loaded) {
- await loadChildren(fullPath, children);
- li.dataset.loaded = '1';
- }
- }
- });
-
- // 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';
- const a = document.createElement('a');
- a.href = '#';
- a.textContent = name;
- a.onclick = (e) => { e.preventDefault(); };
- const add = document.createElement('button');
- add.textContent = '+';
- add.title = 'Add file to selection';
- add.onclick = () => addFileToSelection(fullPath);
- li.appendChild(a);
- li.appendChild(add);
- return li;
-}
-
-async function loadChildren(parentPath, ul) {
- const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
- if (r.error) { ul.innerHTML = `${r.error}`; 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) {
- el('tree').innerHTML = '';
- const display = p.split('/').filter(Boolean).slice(-1)[0] || p;
- el('tree').appendChild(renderTree(display, p));
- updateSelectionList();
-}
-
-function updateSelectionList() {
- el('selCount').textContent = String(selected.length);
- const ul = el('selected');
- ul.innerHTML = '';
- for (const p of selected) {
- const li = document.createElement('li');
- li.textContent = p;
- const btn = document.createElement('button');
- btn.textContent = 'remove';
- btn.onclick = () => { selected = selected.filter(x => x !== p); updateSelectionList(); };
- li.appendChild(btn);
- ul.appendChild(li);
- }
- // naive token estimator ~ 4 chars/token
- const tokens = Math.ceil(selected.join('\n').length / 4);
- el('tokenCount').textContent = String(Math.ceil(tokens));
-}
-
-function addToSelection(p) {
- if (!selected.includes(p)) { selected.push(p); updateSelectionList(); }
-}
-
-
-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(); }
-}
-
-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(); }
-}
-
-
-// Theme persistence and toggle
-(function initTheme() {
- const saved = localStorage.getItem('hero-theme');
- const root = document.documentElement;
- if (saved === 'light') root.classList.add('light');
-})();
-
-el('toggleTheme').onclick = () => {
- const root = document.documentElement;
- const isLight = root.classList.toggle('light');
- localStorage.setItem('hero-theme', isLight ? 'light' : 'dark');
-};
-
-// Workspaces list + selector
-async function reloadWorkspaces() {
- const sel = document.getElementById('workspaceSelect');
- if (!sel) return;
- sel.innerHTML = '';
- const names = await api('/api/heroprompt/workspaces').catch(() => []);
- for (const n of names || []) {
- const opt = document.createElement('option');
- opt.value = n; opt.textContent = n;
- sel.appendChild(opt);
- }
- // ensure current ws name exists or select first
- function updateWsInfo(info) { const box = document.getElementById('wsInfo'); if (!box) return; if (!info || info.error) { box.textContent = ''; return; } box.textContent = `${info.name} — ${info.base_path}`; }
-
- if ([...sel.options].some(o => o.value === currentWs)) sel.value = currentWs;
- else if (sel.options.length > 0) sel.value = sel.options[0].value;
-}
-// On initial load: pick current or first workspace and load its base
-(async function initWorkspace() {
- const sel = document.getElementById('workspaceSelect');
- const names = await api('/api/heroprompt/workspaces').catch(() => []);
- if (!names || names.length === 0) return;
- if (!currentWs || !names.includes(currentWs)) { currentWs = names[0]; localStorage.setItem('heroprompt-current-ws', currentWs); }
- if (sel) sel.value = currentWs;
- const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
- const base = info?.base_path || '';
- if (base) await loadDir(base);
-})();
-// Create workspace modal wiring
-const wcShow = () => { el('wcName').value = ''; el('wcPath').value = ''; el('wcError').textContent = ''; showModal('wsCreate'); };
-el('wsCreateBtn')?.addEventListener('click', wcShow);
-el('wcClose')?.addEventListener('click', () => hideModal('wsCreate'));
-el('wcCancel')?.addEventListener('click', () => hideModal('wsCreate'));
-
-el('wcCreate')?.addEventListener('click', async () => {
- const name = el('wcName').value.trim();
- const path = el('wcPath').value.trim();
- if (!path) { el('wcError').textContent = 'Path is required.'; return; }
- const formData = { base_path: path };
- if (name) formData.name = name;
- const resp = await post('/api/heroprompt/workspaces', formData);
- if (resp.error) { el('wcError').textContent = resp.error; return; }
- currentWs = resp.name || currentWs;
- localStorage.setItem('heroprompt-current-ws', currentWs);
- await reloadWorkspaces();
- const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
- const base = info?.base_path || '';
- if (base) await loadDir(base);
- hideModal('wsCreate');
-});
-// Workspace details modal
-el('wsDetailsBtn')?.addEventListener('click', async () => {
- const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
- if (info && !info.error) { el('wdName').value = info.name || currentWs; el('wdPath').value = info.base_path || ''; el('wdError').textContent = ''; showModal('wsDetails'); }
-});
-
-el('wdClose')?.addEventListener('click', () => hideModal('wsDetails'));
-el('wdCancel')?.addEventListener('click', () => hideModal('wsDetails'));
-
-el('wdSave')?.addEventListener('click', async () => {
- const newName = el('wdName').value.trim();
- const newPath = el('wdPath').value.trim();
- // update via create semantics if name changed, or add an update endpoint later
- const form = new FormData(); if (newName) form.append('name', newName); if (newPath) form.append('base_path', newPath);
- const resp = await fetch('/api/heroprompt/workspaces', { method: 'POST', body: form });
- const j = await resp.json().catch(() => ({ error: 'request failed' }));
- if (j.error) { el('wdError').textContent = j.error; return; }
- currentWs = j.name || newName || currentWs; localStorage.setItem('heroprompt-current-ws', currentWs);
- await reloadWorkspaces();
- const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
- const base = info?.base_path || '';
- if (base) await loadDir(base);
- hideModal('wsDetails');
-});
-
-el('wdDelete')?.addEventListener('click', async () => {
- // simple delete through factory delete via dedicated endpoint would be ideal; for now we can implement a delete endpoint later
- const ok = confirm('Delete this workspace?'); if (!ok) return;
- const r = await fetch(`/api/heroprompt/workspaces/${currentWs}`, { method: 'DELETE' });
- const j = await r.json().catch(() => ({}));
- // ignore errors for now
- await reloadWorkspaces();
- const sel = document.getElementById('workspaceSelect');
- currentWs = sel?.value || '';
- localStorage.setItem('heroprompt-current-ws', currentWs);
- if (currentWs) { const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const base = info?.base_path || ''; if (base) await loadDir(base); }
- hideModal('wsDetails');
-});
-
-
-
-
-if (document.getElementById('workspaceSelect')) {
- // Copy Prompt: generate on server using workspace.prompt and copy to clipboard
- el('copyPrompt')?.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);
- } catch (e) {
- console.warn('copy prompt failed', e);
- }
- });
-
- reloadWorkspaces();
- document.getElementById('workspaceSelect').addEventListener('change', async (e) => {
- currentWs = e.target.value;
- localStorage.setItem('heroprompt-current-ws', currentWs);
- const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
- const base = info?.base_path || '';
- if (base) await loadDir(base);
- });
-}
-
-document.getElementById('refreshWs')?.addEventListener('click', async () => {
- const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
- const base = info?.base_path || '';
- if (base) await loadDir(base);
-});
-document.getElementById('openWsManage')?.addEventListener('click', async () => {
- // populate manage list and open
- const list = el('wmList'); const err = el('wmError'); if (!list) return;
- err.textContent = ''; list.innerHTML = '';
- const names = await api('/api/heroprompt/workspaces').catch(() => []);
- for (const n of names || []) { const li = document.createElement('li'); const s = document.createElement('span'); s.textContent = n; const b = document.createElement('button'); b.className = 'use'; b.textContent = 'Use'; b.onclick = async () => { currentWs = n; await reloadWorkspaces(); const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const base = info?.base_path || ''; if (base) await loadDir(base); hideModal('wsManage'); }; li.appendChild(s); li.appendChild(b); list.appendChild(li); }
- showModal('wsManage');
-});
-
-// legacy setWs kept for backward compat - binds currentWs
-el('setWs')?.addEventListener('click', async () => {
- const base = el('basePath')?.value?.trim();
- if (!base) { alert('Enter base path'); return; }
- const r = await post('/api/heroprompt/workspaces', { name: currentWs, base_path: base });
- if (r.error) { alert(r.error); return; }
- await loadDir(base);
-});
-
-el('doSearch').onclick = async () => {
- const q = el('search').value.trim();
- if (!q) return;
- const r = await api(`/api/heroprompt/search?name=${currentWs}&q=${encodeURIComponent(q)}`);
- if (r.error) { alert(r.error); return; }
- const tree = el('tree');
- tree.innerHTML = 'Search results:
';
- const ul = document.createElement('ul');
- for (const it of r) {
- const li = document.createElement('li');
- li.className = it.type;
- const a = document.createElement('a');
- a.href = '#'; a.textContent = it.path;
- a.onclick = async (e) => {
- e.preventDefault();
- if (it.type === 'file') {
- const rf = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(it.path)}`);
- if (!rf.error) el('preview').textContent = rf.content;
- } else {
- await loadDir(it.path);
- }
- };
- const add = document.createElement('button');
- add.textContent = '+';
- add.title = 'Add to selection';
- add.onclick = () => addToSelection(it.path);
- li.appendChild(a);
- li.appendChild(add);
- ul.appendChild(li);
- }
- tree.appendChild(ul);
-};
-
-// Tabs
-function switchTab(id) {
- for (const t of document.querySelectorAll('.tab')) t.classList.remove('active');
- for (const p of document.querySelectorAll('.tab-pane')) p.classList.remove('active');
- const btn = document.querySelector(`.tab[data-tab="${id}"]`);
- const pane = document.getElementById(`tab-${id}`);
- if (btn && pane) {
- btn.classList.add('active');
- pane.classList.add('active');
- }
-}
-
-for (const btn of document.querySelectorAll('.tab')) {
- btn.addEventListener('click', () => switchTab(btn.dataset.tab));
-}
-
-// Chat (client-side mock for now)
-el('sendChat').onclick = () => {
- const input = el('chatInput');
- const text = input.value.trim();
- if (!text) return;
- addChatMessage('user', text);
- input.value = '';
- // Mock AI response
- setTimeout(() => addChatMessage('ai', 'This is a placeholder AI response.'), 500);
-};
-
-function addChatMessage(role, text) {
- const msg = document.createElement('div');
- msg.className = `message ${role}`;
- const bubble = document.createElement('div');
- bubble.className = 'bubble';
- bubble.textContent = text;
- msg.appendChild(bubble);
- el('chatMessages').appendChild(msg);
- el('chatMessages').scrollTop = el('chatMessages').scrollHeight;
-}
-
-// Modal helpers
-function showModal(id) { const m = el(id); if (!m) return; m.setAttribute('aria-hidden', 'false'); }
-function hideModal(id) { const m = el(id); if (!m) return; m.setAttribute('aria-hidden', 'true'); el('wsError').textContent = ''; }
-
-
-
-
-
-
diff --git a/lib/web/ui/chat/endpoints.v b/lib/web/ui/chat_endpoints.v
similarity index 67%
rename from lib/web/ui/chat/endpoints.v
rename to lib/web/ui/chat_endpoints.v
index 952ca743..41670468 100644
--- a/lib/web/ui/chat/endpoints.v
+++ b/lib/web/ui/chat_endpoints.v
@@ -1,20 +1,19 @@
-module chat
+module ui
import os
-import freeflowuniverse.herolib.web.ui
-pub fn render(app &ui.App) !string {
+pub fn render_chat_alt(app &App) !string {
tpl := os.join_path(os.dir(@FILE), 'templates', 'chat.html')
content := os.read_file(tpl)!
- menu_content := ui.menu_html(app.menu, 0, 'm')
+ menu_content := menu_html(app.menu, 0, 'm')
mut result := content
result = result.replace('{{.title}}', app.title)
result = result.replace('{{.menu_html}}', menu_content)
result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
result = result.replace('{{.css_main_url}}', '/static/css/main.css')
- result = result.replace('{{.css_chat_url}}', '/static/chat/css/chat.css')
+ result = result.replace('{{.css_chat_url}}', '/static/css/chat.css')
result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
- result = result.replace('{{.js_chat_url}}', '/static/chat/js/chat.js')
+ result = result.replace('{{.js_chat_url}}', '/static/js/chat.js')
// version banner
result = result.replace('
-
-
-
-
-
-
-
-
Heroprompt
- /admin/heroprompt
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Select a workspace to view its directories and files.
-
-
-
-
-
-
-
-
-
-
-
-
-
- These instructions will be included in the generated prompt along with the selected
- files.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-', '
Rendered by: chat
')
return result
diff --git a/lib/web/ui/chat/utils.v b/lib/web/ui/chat_utils.v
similarity index 77%
rename from lib/web/ui/chat/utils.v
rename to lib/web/ui/chat_utils.v
index 127b3671..79ca78a8 100644
--- a/lib/web/ui/chat/utils.v
+++ b/lib/web/ui/chat_utils.v
@@ -1,4 +1,3 @@
-module chat
+module ui
// Placeholder for chat-specific utilities
-
diff --git a/lib/web/ui/heroprompt/static/css/heroprompt.css b/lib/web/ui/heroprompt/static/css/heroprompt.css
deleted file mode 100644
index 4694b6b7..00000000
--- a/lib/web/ui/heroprompt/static/css/heroprompt.css
+++ /dev/null
@@ -1,267 +0,0 @@
-/* Heroprompt-specific styles */
-
-.heroprompt-container {
- display: flex;
- gap: 1rem;
- height: calc(100vh - 200px);
-}
-
-.workspaces-panel {
- flex: 0 0 280px;
- min-width: 280px;
-}
-
-.workspace-details {
- flex: 1;
- min-width: 0;
-}
-
-.instructions-panel {
- flex: 0 0 350px;
- min-width: 350px;
-}
-
-/* Workspace list styling */
-.workspace-item {
- cursor: pointer;
- transition: background-color 0.2s ease;
- border: none !important;
- padding: 0.75rem 1rem;
-}
-
-.workspace-item:hover {
- background-color: var(--bs-gray-100);
-}
-
-[data-bs-theme="dark"] .workspace-item:hover {
- background-color: var(--bs-gray-800);
-}
-
-.workspace-item.active {
- background-color: var(--bs-primary);
- color: white;
-}
-
-.workspace-item.active:hover {
- background-color: var(--bs-primary);
-}
-
-/* Directory and file styling */
-.directory-item {
- border: 1px solid var(--bs-border-color);
- border-radius: 0.375rem;
- margin-bottom: 1rem;
- background-color: var(--bs-body-bg);
-}
-
-.directory-header {
- background-color: var(--bs-gray-50);
- border-bottom: 1px solid var(--bs-border-color);
- padding: 0.75rem 1rem;
- font-weight: 600;
- border-radius: 0.375rem 0.375rem 0 0;
-}
-
-[data-bs-theme="dark"] .directory-header {
- background-color: var(--bs-gray-800);
-}
-
-.file-list {
- padding: 0.5rem;
- max-height: 300px;
- overflow-y: auto;
-}
-
-.file-item {
- display: flex;
- align-items: center;
- padding: 0.5rem;
- border-radius: 0.25rem;
- cursor: pointer;
- transition: background-color 0.2s ease;
- margin-bottom: 0.25rem;
-}
-
-.file-item:hover {
- background-color: var(--bs-gray-100);
-}
-
-[data-bs-theme="dark"] .file-item:hover {
- background-color: var(--bs-gray-700);
-}
-
-.file-item.selected {
- background-color: var(--bs-success-bg-subtle);
- border: 1px solid var(--bs-success-border-subtle);
-}
-
-.file-item input[type="checkbox"] {
- margin-right: 0.5rem;
-}
-
-.file-name {
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 0.875rem;
-}
-
-/* Empty state styling */
-.empty-state {
- text-align: center;
- padding: 2rem;
- color: var(--bs-text-muted);
-}
-
-.empty-state-icon {
- font-size: 3rem;
- margin-bottom: 1rem;
- opacity: 0.5;
-}
-
-/* Button styling */
-.btn-group-actions {
- display: flex;
- gap: 0.5rem;
- align-items: center;
-}
-
-/* Toast styling */
-.toast {
- min-width: 300px;
-}
-
-/* Instructions panel styling */
-.instructions-panel textarea {
- resize: vertical;
- min-height: 200px;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 0.875rem;
- line-height: 1.5;
-}
-
-.instructions-panel .card-body {
- padding: 1rem;
-}
-
-/* Responsive design */
-@media (max-width: 1200px) {
- .heroprompt-container {
- flex-direction: column;
- height: auto;
- }
-
- .workspaces-panel,
- .workspace-details,
- .instructions-panel {
- flex: none;
- min-width: auto;
- margin-bottom: 1rem;
- }
-
- .instructions-panel {
- order: 3;
- }
-}
-
-@media (max-width: 768px) {
- .heroprompt-container {
- flex-direction: column;
- height: auto;
- }
-
- .workspaces-panel,
- .workspace-details,
- .instructions-panel {
- flex: none;
- min-width: auto;
- margin-bottom: 1rem;
- }
-}
-
-/* Selection counter */
-.selection-counter {
- font-size: 0.875rem;
- color: var(--bs-text-muted);
- margin-left: 0.5rem;
-}
-
-.selection-counter.has-selection {
- color: var(--bs-success);
- font-weight: 600;
-}
-
-/* Directory path styling */
-.directory-path {
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 0.75rem;
- color: var(--bs-text-muted);
- margin-top: 0.25rem;
-}
-
-/* Workspace name input */
-.workspace-name-input {
- border: 1px solid var(--bs-border-color);
- border-radius: 0.25rem;
- padding: 0.5rem;
- width: 100%;
- margin-bottom: 0.5rem;
-}
-
-/* Loading state */
-.loading-spinner {
- display: inline-block;
- width: 1rem;
- height: 1rem;
- border: 2px solid var(--bs-border-color);
- border-radius: 50%;
- border-top-color: var(--bs-primary);
- animation: spin 1s ease-in-out infinite;
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-/* File type icons */
-.file-icon {
- margin-right: 0.5rem;
- width: 16px;
- text-align: center;
-}
-
-.file-icon.file-md::before {
- content: "📝";
-}
-
-.file-icon.file-v::before {
- content: "⚡";
-}
-
-.file-icon.file-js::before {
- content: "📜";
-}
-
-.file-icon.file-css::before {
- content: "🎨";
-}
-
-.file-icon.file-html::before {
- content: "🌐";
-}
-
-.file-icon.file-json::before {
- content: "📋";
-}
-
-.file-icon.file-yaml::before {
- content: "⚙️";
-}
-
-.file-icon.file-txt::before {
- content: "📄";
-}
-
-.file-icon.file-default::before {
- content: "📁";
-}
\ No newline at end of file
diff --git a/lib/web/ui/heroprompt/static/js/heroprompt.js b/lib/web/ui/heroprompt/static/js/heroprompt.js
deleted file mode 100644
index 1353ae66..00000000
--- a/lib/web/ui/heroprompt/static/js/heroprompt.js
+++ /dev/null
@@ -1,760 +0,0 @@
-/**
- * Heroprompt - Client-side workspace and file selection management
- * Updated to work with V backend API and support subdirectories
- */
-
-class Heroprompt {
- constructor() {
- // Backend-integrated state (no localStorage)
- this.currentWorkspace = '';
- this.workspaces = [];
- this.selectedFiles = new Set();
- this.selectedDirs = new Set();
-
- this.initializeUI();
- this.bindEvents();
- // Load workspaces from backend and render
- this.refreshWorkspaces();
- }
-
- // Data management
- loadData() {
- try {
- const stored = localStorage.getItem(this.storageKey);
- if (stored) {
- return JSON.parse(stored);
- }
- } catch (e) {
- console.warn('Failed to load heroprompt data:', e);
- }
-
- return {
- workspaces: {
- default: { dirs: [] }
- },
- current: 'default'
- };
- }
-
- saveData() {
- try {
- localStorage.setItem(this.storageKey, JSON.stringify(this.data));
- } catch (e) {
- console.error('Failed to save heroprompt data:', e);
- this.showToast('Failed to save data', 'error');
- }
- }
-
- // API calls to V backend
- async fetchDirectory(path) {
- try {
- const qs = new URLSearchParams({ name: this.currentWorkspace || 'default', path }).toString();
- const response = await fetch(`/api/heroprompt/directory?${qs}`);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- return await response.json();
- } catch (e) {
- console.error('Failed to fetch directory:', e);
- throw e;
- }
- }
-
- async fetchFileContent(path) {
- try {
- const qs = new URLSearchParams({ name: this.currentWorkspace || 'default', path }).toString();
- const response = await fetch(`/api/heroprompt/file?${qs}`);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- return await response.json();
- } catch (e) {
- console.error('Failed to fetch file:', e);
- throw e;
- }
- }
-
- // Workspace management
- createWorkspace(name) {
- if (!name || name.trim() === '') {
- this.showToast('Workspace name cannot be empty', 'error');
- return;
- }
-
- name = name.trim();
- if (this.data.workspaces[name]) {
- this.showToast('Workspace already exists', 'error');
- return;
- }
-
- this.data.workspaces[name] = { dirs: [] };
- this.data.current = name;
- this.currentWorkspace = name;
- this.saveData();
- this.render();
- this.showToast(`Workspace "${name}" created`, 'success');
- }
-
- deleteWorkspace(name) {
- if (name === 'default') {
- this.showToast('Cannot delete default workspace', 'error');
- return;
- }
-
- if (!confirm(`Are you sure you want to delete workspace "${name}"?`)) {
- return;
- }
-
- delete this.data.workspaces[name];
-
- if (this.currentWorkspace === name) {
- this.currentWorkspace = 'default';
- this.data.current = 'default';
- }
-
- this.saveData();
- this.render();
- this.showToast(`Workspace "${name}" deleted`, 'success');
- }
-
- selectWorkspace(name) {
- if (!this.data.workspaces[name]) {
- this.showToast('Workspace not found', 'error');
- return;
- }
-
- this.currentWorkspace = name;
- this.data.current = name;
- this.saveData();
- this.render();
- }
-
- // Directory management
- async addDirectory() {
- // Try to use the File System Access API if available
- if ('showDirectoryPicker' in window) {
- await this.addDirectoryWithPicker();
- } else {
- this.addDirectoryWithPrompt();
- }
- }
- // Backend API helpers
- async apiListWorkspaces() {
- const res = await fetch('/api/heroprompt/workspaces');
- if (!res.ok) throw new Error('Failed to list workspaces');
- return await res.json(); // array of names
- }
-
- async apiCreateWorkspace(name, base_path = '') {
- const form = new FormData();
- form.append('name', name);
- if (base_path) form.append('base_path', base_path);
- const res = await fetch('/api/heroprompt/workspaces', { method: 'POST', body: form });
- if (!res.ok) throw new Error('Failed to create workspace');
- return await res.json();
- }
-
- async apiDeleteWorkspace(name) {
- const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(name)}`, { method: 'DELETE' });
- if (!res.ok) throw new Error('Failed to delete workspace');
- return await res.json();
- }
-
- async refreshWorkspaces(selectName = '') {
- try {
- this.workspaces = await this.apiListWorkspaces();
- if (this.workspaces.length > 0) {
- this.currentWorkspace = selectName || this.currentWorkspace || this.workspaces[0];
- } else {
- this.currentWorkspace = '';
- }
- await this.render();
- } catch (e) {
- console.error(e);
- this.showToast('Failed to load workspaces', 'error');
- }
- }
- async apiGetWorkspace(name) {
- const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(name)}`);
- if (!res.ok) throw new Error('Failed to get workspace');
- return await res.json();
- }
-
- async apiAddDir(path) {
- const form = new FormData();
- form.append('path', path);
- const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/dirs`, {
- method: 'POST', body: form
- });
- if (!res.ok) throw new Error('Failed to add directory');
- return await res.json();
- }
-
- async apiRemoveDir(path) {
- const form = new FormData();
- form.append('path', path);
- const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/dirs/remove`, {
- method: 'POST', body: form
- });
- if (!res.ok) throw new Error('Failed to remove directory');
- return await res.json();
- }
-
- async apiAddFile(path) {
- const form = new FormData();
- form.append('path', path);
- const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/files`, {
- method: 'POST', body: form
- });
- if (!res.ok) throw new Error('Failed to add file');
- return await res.json();
- }
-
- async apiRemoveFile(path) {
- const form = new FormData();
- form.append('path', path);
- const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/files/remove`, {
- method: 'POST', body: form
- });
- if (!res.ok) throw new Error('Failed to remove file');
- return await res.json();
- }
-
- async loadCurrentWorkspaceDetails() {
- if (!this.currentWorkspace) {
- this.currentDetails = null;
- this.selectedFiles = new Set();
- this.selectedDirs = new Set();
- return;
- }
- const data = await this.apiGetWorkspace(this.currentWorkspace);
- this.currentDetails = data;
- const files = new Set();
- const dirs = new Set();
- for (const ch of (data.children || [])) {
- if (ch.type === 'file') files.add(ch.path);
- if (ch.type === 'directory') dirs.add(ch.path);
- }
- this.selectedFiles = files;
- this.selectedDirs = dirs;
- }
-
-
-
- async addDirectoryWithPicker() {
- try {
- const dirHandle = await window.showDirectoryPicker();
-
- // For File System Access API, we need to read the directory contents directly
- // since we can't pass the handle to the backend
- const files = [];
- const subdirs = [];
-
- for await (const [name, handle] of dirHandle.entries()) {
- if (handle.kind === 'file') {
- files.push(name);
- } else if (handle.kind === 'directory') {
- subdirs.push(name);
- }
- }
-
- const processedDir = {
- path: dirHandle.name, // Use the directory name as path for now
- files: files.sort(),
- subdirs: subdirs.sort(),
- selected: []
- };
-
- this.data.workspaces[this.currentWorkspace].dirs.push(processedDir);
- this.saveData();
- this.render();
- this.showToast(`Directory "${dirHandle.name}" added`, 'success');
-
- } catch (e) {
- if (e.name !== 'AbortError') {
- console.error('Directory picker error:', e);
- this.showToast('Failed to access directory', 'error');
- }
- }
- }
-
- async processDirectoryHandle(dirHandle, basePath = '') {
- // This method is no longer used with the File System Access API approach
- // but kept for compatibility
- try {
- const fullPath = basePath ? `${basePath}/${dirHandle.name}` : dirHandle.name;
- const dirData = await this.fetchDirectory(fullPath);
-
- // Check if we got an error response
- if (dirData.error) {
- throw new Error(dirData.error);
- }
-
- // Process the directory structure from the backend
- const processedDir = {
- path: dirData.path,
- files: [],
- subdirs: [],
- selected: []
- };
-
- // Separate files and directories
- if (dirData.items && Array.isArray(dirData.items)) {
- for (const item of dirData.items) {
- if (item.type === 'file') {
- processedDir.files.push(item.name);
- } else if (item.type === 'directory') {
- processedDir.subdirs.push(item.name);
- }
- }
- }
-
- this.data.workspaces[this.currentWorkspace].dirs.push(processedDir);
- this.saveData();
- this.render();
- this.showToast(`Directory "${dirHandle.name}" added`, 'success');
-
- } catch (e) {
- console.error('Failed to process directory:', e);
- this.showToast('Failed to process directory', 'error');
- }
- }
-
- async addDirectoryWithPrompt() {
- const path = prompt('Enter directory path:');
- if (!path || path.trim() === '') return;
- try {
- await this.apiAddDir(path.trim());
- await this.render();
- this.showToast(`Directory "${path}" added`, 'success');
- } catch (e) {
- console.error('Failed to add directory:', e);
- this.showToast(`Failed to add directory: ${e.message}`, 'error');
- }
- }
-
- async removeDirectory(path) {
- if (!confirm('Are you sure you want to remove this directory?')) return;
- try {
- await this.apiRemoveDir(path);
- await this.render();
- this.showToast('Directory removed', 'success');
- } catch (e) {
- console.error('Failed to remove directory:', e);
- this.showToast('Failed to remove directory', 'error');
- }
- }
-
- // File selection management (backend-synced)
- async toggleFileSelection(filePath) {
- try {
- if (this.selectedFiles.has(filePath)) {
- await this.apiRemoveFile(filePath);
- this.selectedFiles.delete(filePath);
- } else {
- await this.apiAddFile(filePath);
- this.selectedFiles.add(filePath);
- }
- await this.renderWorkspaceDetails();
- } catch (e) {
- console.error('Failed to toggle file selection:', e);
- this.showToast('Failed to toggle file selection', 'error');
- }
- }
-
- selectAllFiles(workspaceName, dirIndex) {
- const dir = this.data.workspaces[workspaceName].dirs[dirIndex];
- dir.selected = [...dir.files];
- this.saveData();
- this.renderWorkspaceDetails();
- }
-
- deselectAllFiles(workspaceName, dirIndex) {
- const dir = this.data.workspaces[workspaceName].dirs[dirIndex];
- dir.selected = [];
- this.saveData();
- this.renderWorkspaceDetails();
- }
-
- // Generate file tree structure
- generateFileTree(dirPath, files, subdirs) {
- const lines = [];
- const dirName = dirPath.split('/').pop() || dirPath;
-
- lines.push(`${dirPath}`);
-
- // Add files
- files.forEach((file, index) => {
- const isLast = index === files.length - 1 && subdirs.length === 0;
- lines.push(`${isLast ? '└──' : '├──'} ${file}`);
- });
-
- // Add subdirectories (placeholder for now)
- subdirs.forEach((subdir, index) => {
- const isLast = index === subdirs.length - 1;
- lines.push(`${isLast ? '└──' : '├──'} ${subdir}/`);
- });
-
- return lines.join('\n');
- }
-
- // Clipboard functionality with new format
- async copySelection() {
- if (!this.currentWorkspace) {
- this.showToast('Select a workspace first', 'error');
- return;
- }
-
- const userInstructions = document.getElementById('user-instructions').value.trim();
- if (!userInstructions) {
- this.showToast('Please enter user instructions', 'error');
- return;
- }
-
- let hasSelection = false;
- const output = [];
-
- // Add user instructions
- output.push('');
- output.push(userInstructions);
- output.push('');
- output.push('');
-
- // Generate file map
- output.push('');
- const dirSet = new Set(Array.from(this.selectedFiles).map(p => p.split('/').slice(0, -1).join('/')));
- for (const dirPath of dirSet) {
- const data = await this.fetchDirectory(dirPath);
- const files = (data.items || []).filter(it => it.type === 'file').map(it => it.name);
- const subdirs = (data.items || []).filter(it => it.type === 'directory').map(it => it.name);
- const fileTree = this.generateFileTree(dirPath, files, subdirs);
- output.push(fileTree);
- }
- output.push('');
- output.push('');
-
- if (this.selectedFiles.size === 0) {
- this.showToast('No files selected', 'error');
- return;
- }
-
- // Generate file contents
- output.push('');
-
- try {
- for (const filePath of Array.from(this.selectedFiles)) {
- try {
- const fileData = await this.fetchFileContent(filePath);
- output.push(`File: ${filePath}`);
- output.push('');
- output.push(`\`\`\`${fileData.language}`);
- output.push(fileData.content);
- output.push('```');
- output.push('');
- } catch (e) {
- console.error(`Failed to fetch file ${filePath}:`, e);
- output.push(`File: ${filePath}`);
- output.push('');
- output.push('```text');
- output.push(`Error: Failed to read file - ${e.message}`);
- output.push('```');
- output.push('');
- }
- }
- } catch (e) {
- console.error('Error fetching file contents:', e);
- this.showToast('Error fetching file contents', 'error');
- return;
- }
-
- output.push('');
-
- const text = output.join('\n');
-
- if (navigator.clipboard && navigator.clipboard.writeText) {
- try {
- await navigator.clipboard.writeText(text);
- this.showToast('Selection copied to clipboard', 'success');
- } catch (e) {
- console.error('Clipboard error:', e);
- this.fallbackCopyToClipboard(text);
- }
- } else {
- this.fallbackCopyToClipboard(text);
- }
- }
-
- fallbackCopyToClipboard(text) {
- const textArea = document.createElement('textarea');
- textArea.value = text;
- textArea.style.position = 'fixed';
- textArea.style.left = '-999999px';
- textArea.style.top = '-999999px';
- document.body.appendChild(textArea);
- textArea.focus();
- textArea.select();
-
- try {
- document.execCommand('copy');
- this.showToast('Selection copied to clipboard', 'success');
- } catch (e) {
- console.error('Fallback copy failed:', e);
- this.showToast('Failed to copy to clipboard', 'error');
- }
-
- document.body.removeChild(textArea);
- }
-
- // UI Management
- initializeUI() {
- // Cache DOM elements
- this.elements = {
- workspaceSelect: document.getElementById('workspace-select'),
- workspaceContent: document.getElementById('workspace-content'),
- currentWorkspaceName: document.getElementById('current-workspace-name'),
- createWorkspaceBtn: document.getElementById('create-workspace'),
- deleteWorkspaceBtn: document.getElementById('delete-workspace'),
- addDirectoryBtn: document.getElementById('add-directory'),
- copySelectionBtn: document.getElementById('copy-selection'),
- clearInstructionsBtn: document.getElementById('clear-instructions'),
- userInstructions: document.getElementById('user-instructions'),
- toast: document.getElementById('notification-toast')
- };
- }
-
- bindEvents() {
- // Workspace management
- this.elements.createWorkspaceBtn.addEventListener('click', async () => {
- const name = prompt('Enter workspace name:');
- if (!name) return;
- const base_path = prompt('Enter base path for this workspace (optional):') || '';
- try {
- await this.apiCreateWorkspace(name.trim(), base_path.trim());
- await this.refreshWorkspaces(name.trim());
- this.showToast(`Workspace "${name}" created`, 'success');
- } catch (e) {
- console.error(e);
- this.showToast('Failed to create workspace', 'error');
- }
- });
-
- this.elements.deleteWorkspaceBtn.addEventListener('click', async () => {
- if (!this.currentWorkspace) return;
- if (!confirm(`Are you sure you want to delete workspace "${this.currentWorkspace}"?`)) return;
- try {
- await this.apiDeleteWorkspace(this.currentWorkspace);
- await this.refreshWorkspaces();
- this.showToast('Workspace deleted', 'success');
- } catch (e) {
- console.error(e);
- this.showToast('Failed to delete workspace', 'error');
- }
- });
-
- this.elements.workspaceSelect.addEventListener('change', async () => {
- this.currentWorkspace = this.elements.workspaceSelect.value;
- await this.render();
- });
-
- // Directory management
- this.elements.addDirectoryBtn.addEventListener('click', () => {
- this.addDirectory();
- });
-
- // Copy selection
- this.elements.copySelectionBtn.addEventListener('click', () => {
- this.copySelection();
- });
-
- // Clear instructions
- this.elements.clearInstructionsBtn.addEventListener('click', () => {
- this.elements.userInstructions.value = '';
- });
- }
-
- render() {
- this.renderWorkspaceSelect();
- this.renderWorkspaceDetails();
- }
-
- renderWorkspaceSelect() {
- const names = this.workspaces;
- const options = names.map(n => ``).join('');
- this.elements.workspaceSelect.innerHTML = options;
- this.elements.deleteWorkspaceBtn.style.display = this.currentWorkspace && this.currentWorkspace !== 'default' ? 'inline-block' : 'none';
- this.elements.currentWorkspaceName.textContent = this.currentWorkspace || 'Select a workspace';
- }
-
- renderWorkspaceDetails() {
- const workspace = this.data.workspaces[this.currentWorkspace];
-
- // Update header
- this.elements.currentWorkspaceName.textContent = this.currentWorkspace;
-
- // Show/hide buttons based on selection
- const hasWorkspace = !!this.currentWorkspace;
- this.elements.deleteWorkspaceBtn.style.display = hasWorkspace && this.currentWorkspace !== 'default' ? 'inline-block' : 'none';
- this.elements.addDirectoryBtn.style.display = hasWorkspace ? 'inline-block' : 'none';
- this.elements.copySelectionBtn.style.display = hasWorkspace ? 'inline-block' : 'none';
-
- if (!workspace) {
- this.elements.workspaceContent.innerHTML = '
Select a workspace to view its directories and files.
';
- return;
- }
-
- if (workspace.dirs.length === 0) {
- this.elements.workspaceContent.innerHTML = `
-
-
📁
-
No directories added to this workspace.
-
Click "Add Directory" to get started.
-
- `;
- return;
- }
-
- // Load current workspace details from backend
- try {
- await this.loadCurrentWorkspaceDetails();
- } catch (e) {
- console.error(e);
- this.elements.workspaceContent.innerHTML = '
Failed to load workspace details.
';
- return;
- }
-
- const dirs = Array.from(this.selectedDirs);
- if (dirs.length === 0) {
- this.elements.workspaceContent.innerHTML = `
-
-
📁
-
No directories added to this workspace.
-
Click "Add Directory" to get started.
-
- `;
- return;
- }
-
- // Render directories as expandable tree explorers
- this.elements.workspaceContent.innerHTML = dirs.map((dirPath, dirIndex) => `
-
- `).join('');
-
- // Populate tree nodes asynchronously
- dirs.forEach((dirPath, dirIndex) => {
- const container = document.getElementById(`file-tree-${dirIndex}`);
- this.renderDirNode(dirPath, dirIndex, container, 0);
- });
- }
- // Render a directory node with lazy subdir loading
- async renderDirNode(dirPath, dirIndex, container, level) {
- try {
- // Fetch directory listing from backend
- const data = await this.fetchDirectory(dirPath);
- const items = data.items || [];
-
- // Separate directories and files
- const dirs = items.filter(it => it.type === 'directory').map(it => it.name);
- const files = items.filter(it => it.type === 'file').map(it => it.name);
-
- // Build HTML
- const indent = ' '.repeat(level * 2);
- const list = [];
-
- dirs.forEach(sub => {
- const subPath = `${dirPath}/${sub}`;
- const nodeId = `dir-node-${dirIndex}-${level}-${sub.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
- list.push(`
-
- `);
- });
-
- files.forEach(file => {
- const absPath = `${dirPath}/${file}`;
- const isSel = this.selectedFiles.has(absPath);
- list.push(`
-
-
-
- ${indent}${file}
-
- `);
- });
-
- container.innerHTML = list.join('');
-
- // Bind toggles for lazy load
- container.querySelectorAll('a.toggle').forEach(a => {
- a.addEventListener('click', async (e) => {
- e.preventDefault();
- const targetId = a.getAttribute('data-target');
- const path = a.getAttribute('data-path');
- const target = document.getElementById(targetId);
- if (target.getAttribute('data-loaded') !== '1') {
- await this.renderDirNode(path, dirIndex, target, level + 1);
- target.setAttribute('data-loaded', '1');
- }
- target.style.display = (target.style.display === 'none') ? 'block' : 'none';
- });
- });
- } catch (e) {
- console.error('Failed to render directory node:', e);
- container.innerHTML = `
Failed to load ${dirPath}
`;
- }
- }
-
- getDirectoryName(path) {
- return path.split('/').pop() || path.split('\\').pop() || path;
- }
-
- getFileIconClass(fileName) {
- const ext = fileName.split('.').pop().toLowerCase();
- return `file-${ext}` || 'file-default';
- }
-
- showToast(message, type = 'info') {
- const toast = this.elements.toast;
- const toastBody = toast.querySelector('.toast-body');
-
- toastBody.textContent = message;
-
- // Set toast color based on type
- toast.className = 'toast';
- if (type === 'success') {
- toast.classList.add('text-bg-success');
- } else if (type === 'error') {
- toast.classList.add('text-bg-danger');
- } else {
- toast.classList.add('text-bg-info');
- }
-
- const bsToast = new bootstrap.Toast(toast);
- bsToast.show();
- }
-}
-
-// Initialize when DOM is loaded
-document.addEventListener('DOMContentLoaded', () => {
- window.heroprompt = new Heroprompt();
-});
-
-// Export for potential external use
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = Heroprompt;
-}
\ No newline at end of file
diff --git a/lib/web/ui/heroprompt/templates/heroprompt.html b/lib/web/ui/heroprompt/templates/heroprompt.html
deleted file mode 100644
index 8345b825..00000000
--- a/lib/web/ui/heroprompt/templates/heroprompt.html
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-
-
-
-
-
{{.title}} - Heroprompt
-
-
-
-
-
-
-
-
-
-