diff --git a/lib/web/ui/static/css/heroprompt.css b/lib/web/ui/static/css/heroprompt.css index c18af77f..eca3f093 100644 --- a/lib/web/ui/static/css/heroprompt.css +++ b/lib/web/ui/static/css/heroprompt.css @@ -1,243 +1,817 @@ -/* Heroprompt specific styles using UI project theme system */ +/* Enhanced HeroPrompt UI - Clean VSCode-like Design */ -/* Tree view specific styles */ -.tree { - font-size: 0.875rem; +/* Icon definitions using CSS pseudo-elements */ +.icon-collapse::before { + content: "⌄"; } -.tree .chev { - transition: transform 0.15s ease; - display: inline-block; - font-size: 0.75rem; - opacity: var(--menu-chevron-opacity); +.icon-refresh::before { + content: "↻"; } -.tree .toggle:checked+.dir-label .chev { - transform: rotate(90deg); +.icon-settings::before { + content: "⚙"; } -.tree .children { - margin-left: 16px; - border-left: 1px solid var(--border-primary); - padding-left: 8px; +.icon-manage::before { + content: "☰"; } -.tree .dir-label { - cursor: pointer; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - transition: all 0.2s ease; - color: var(--text-primary); +.icon-search::before { + content: "🔍"; } -.tree .dir-label:hover { - background-color: var(--menu-item-hover-bg); - color: var(--menu-item-hover-text); +.icon-close::before { + content: "✕"; } -.tree .file { - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - transition: all 0.2s ease; +.icon-folder-open::before { + content: "📂"; } -.tree .file:hover { - background-color: var(--menu-item-hover-bg); +.icon-folder-closed::before { + content: "📁"; } -.tree .file a { - color: var(--text-primary); - text-decoration: none; - transition: color 0.2s ease; +.icon-file::before { + content: "📄"; } -.tree .file a:hover { - color: var(--link-hover-color); +.icon-selection::before { + content: "☑"; } -/* Tab content styling */ -.tab-pane { - height: calc(100vh - 200px); - overflow-y: auto; +.icon-prompt::before { + content: "✎"; +} + +.icon-chat::before { + content: "💬"; +} + +.icon-token::before { + content: "🔢"; +} + +.icon-export::before { + content: "↗"; +} + +.icon-import::before { + content: "↙"; +} + +.icon-copy::before { + content: "📋"; +} + +.icon-template::before { + content: "📝"; +} + +.icon-save::before { + content: "💾"; +} + +.icon-generate::before { + content: "⚡"; +} + +.icon-clear::before { + content: "🗑"; +} + +.icon-ai::before { + content: "🤖"; +} + +.icon-send::before { + content: "➤"; +} + +.icon-empty::before { + content: "∅"; +} + +/* Base layout improvements */ +.main { background-color: var(--bg-primary); color: var(--text-primary); } -/* Selection list styling */ -#selected { - max-height: 300px; - overflow-y: auto; +/* Explorer Panel */ +.explorer-panel { background-color: var(--bg-secondary); border: 1px solid var(--border-primary); - border-radius: 0.375rem; - padding: 0.5rem; + border-radius: 8px; + display: flex; + flex-direction: column; + overflow: hidden; } -#selected li { - background-color: var(--card-bg); - border: 1px solid var(--card-border); - color: var(--text-primary); -} - -#selected li:hover { +.explorer-header { + padding: 12px 16px; + border-bottom: 1px solid var(--border-primary); background-color: var(--bg-tertiary); } -/* Preview area styling */ -#preview { - background-color: var(--bg-secondary); - border: 1px solid var(--border-primary); - color: var(--text-primary); - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; - font-size: 0.75rem; - white-space: pre-wrap; - line-height: 1.4; - border-radius: 0.375rem; - padding: 0.75rem; -} - -/* Prompt output styling */ -#promptOutput { - background-color: var(--bg-secondary); - border: 1px solid var(--border-primary); - color: var(--text-primary); - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; - font-size: 0.75rem; - white-space: pre-wrap; - line-height: 1.4; - border-radius: 0.375rem; - padding: 0.75rem; -} - -/* Chat messages styling */ -#chatMessages { - background-color: var(--bg-secondary); - border: 1px solid var(--border-primary); - color: var(--text-primary); - border-radius: 0.375rem; - padding: 0.75rem; -} - -/* Form controls theme integration */ -.form-control { - background-color: var(--bg-primary); - border-color: var(--border-primary); - color: var(--text-primary); -} - -.form-control:focus { - background-color: var(--bg-primary); - border-color: var(--link-color); - color: var(--text-primary); - box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); -} - -.form-control::placeholder { - color: var(--text-muted); -} - -/* Select elements */ -.form-select { - background-color: var(--bg-primary); - border-color: var(--border-primary); - color: var(--text-primary); -} - -.form-select:focus { - background-color: var(--bg-primary); - border-color: var(--link-color); - color: var(--text-primary); -} - -/* Button theme integration */ -.btn-outline-secondary { +.explorer-title { + font-size: 11px; + letter-spacing: 0.5px; color: var(--text-secondary); - border-color: var(--border-primary); + margin: 0; } -.btn-outline-secondary:hover { - background-color: var(--bg-tertiary); - border-color: var(--border-secondary); +.explorer-actions { + display: flex; + gap: 4px; +} + +.btn-ghost { + background: transparent; + border: none; + color: var(--text-secondary); + padding: 4px 6px; + border-radius: 4px; + font-size: 12px; + transition: all 0.2s ease; + cursor: pointer; +} + +.btn-ghost:hover { + background-color: var(--menu-item-hover-bg); color: var(--text-primary); } -.btn-outline-primary { - color: var(--link-color); +.workspace-selector { + margin-bottom: 12px; +} + +.modern-select { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + border-radius: 4px; + font-size: 13px; + padding: 6px 8px; +} + +.modern-select:focus { border-color: var(--link-color); + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2); + outline: none; } -.btn-outline-primary:hover { - background-color: var(--link-color); +.search-container { + position: relative; +} + +.search-icon { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-right: none; + color: var(--text-secondary); + font-size: 12px; + padding: 6px 8px; +} + +.modern-input { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-left: none; + border-right: none; + color: var(--text-primary); + font-size: 13px; + padding: 6px 8px; +} + +.modern-input:focus { border-color: var(--link-color); - color: var(--text-light); + box-shadow: none; + outline: none; } -.btn-outline-danger { - color: var(--danger-color); - border-color: var(--danger-color); +.search-clear { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-left: none; + border-radius: 0 4px 4px 0; + padding: 6px 8px; } -.btn-outline-danger:hover { - background-color: var(--danger-color); - border-color: var(--danger-color); - color: var(--text-light); +.explorer-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 4px 6px; } -/* Badge styling */ -.badge { +.selection-controls { + padding: 4px 0; + border-bottom: 1px solid var(--border-primary); +} + +.selection-info .badge-selection { background-color: var(--bg-tertiary); color: var(--text-primary); + font-size: 11px; + padding: 2px 6px; + border-radius: 10px; + border: 1px solid var(--border-primary); } -/* Modal theme integration */ -.modal-content { - background-color: var(--card-bg); - border: 1px solid var(--card-border); - color: var(--text-primary); +.selection-actions { + display: flex; + gap: 8px; } -.modal-header { - border-bottom-color: var(--border-primary); +.btn-xs { + font-size: 11px; + padding: 2px 6px; } -.modal-footer { - border-top-color: var(--border-primary); -} - -/* Alert styling */ -.alert-success { - background-color: rgba(25, 135, 84, 0.1); - border-color: var(--success-color); - color: var(--success-color); -} - -.alert-danger { - background-color: rgba(220, 53, 69, 0.1); - border-color: var(--danger-color); - color: var(--danger-color); -} - -/* Tree node buttons */ -.tree .btn { - font-size: 0.75rem; - padding: 0.125rem 0.375rem; +/* Simple File Tree */ +.file-tree { + flex: 1; + overflow-y: auto; + padding: 2px 0; + font-size: 12px; line-height: 1.2; } -/* Workspace info styling */ -.workspace-info { - background-color: var(--bg-secondary); - border: 1px solid var(--border-primary); - border-radius: 0.375rem; - padding: 0.5rem; - font-size: 0.75rem; - color: var(--text-muted); +.file-tree::-webkit-scrollbar { + width: 6px; } -/* Loading states */ +.file-tree::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +.file-tree::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 3px; +} + +.file-tree::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + color: var(--text-muted); + text-align: center; +} + +.empty-state i { + font-size: 24px; + margin-bottom: 8px; + opacity: 0.5; +} + +.empty-state p { + margin: 0; + font-size: 13px; +} + +/* Tree Items - Clean and Simple */ +.tree-item { + display: block; + margin: 0; + user-select: none; +} + +.tree-item-content { + display: flex; + align-items: center; + padding: 2px 4px; + border-radius: 3px; + cursor: pointer; + transition: background-color 0.15s ease; + min-height: 20px; +} + +.tree-item-content:hover { + background-color: var(--menu-item-hover-bg); +} + +.tree-checkbox { + margin: 0 6px 0 0; + cursor: pointer; + width: 12px; + height: 12px; +} + +.tree-expand-btn { + background: none; + border: none; + color: var(--text-secondary); + font-size: 10px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + margin-right: 2px; + border-radius: 2px; + transition: all 0.15s ease; +} + +.tree-expand-btn:hover { + background-color: var(--bg-tertiary); + color: var(--text-primary); +} + +.tree-expand-spacer { + width: 16px; + height: 16px; + margin-right: 2px; +} + +.tree-icon { + font-size: 12px; + margin-right: 6px; + width: 16px; + text-align: center; +} + +.tree-label { + flex: 1; + font-size: 12px; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; +} + +/* Workspace Panel */ +.workspace-panel { + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 8px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.workspace-header { + background-color: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); + padding: 0; +} + +.tab-navigation { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; +} + +.modern-tabs { + display: flex; + gap: 2px; + border: none; +} + +.modern-tabs .nav-link { + background: transparent; + border: none; + color: var(--text-secondary); + padding: 8px 12px; + border-radius: 6px 6px 0 0; + font-size: 13px; + display: flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; + cursor: pointer; +} + +.modern-tabs .nav-link:hover { + background-color: var(--menu-item-hover-bg); + color: var(--text-primary); +} + +.modern-tabs .nav-link.active { + background-color: var(--bg-primary); + color: var(--text-primary); + border-bottom: 2px solid var(--link-color); +} + +.badge-count { + background-color: var(--bg-tertiary); + color: var(--text-primary); + font-size: 10px; + padding: 1px 4px; + border-radius: 8px; + min-width: 16px; + text-align: center; +} + +.workspace-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.token-counter { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--text-secondary); +} + +.workspace-content { + flex: 1; + overflow: hidden; +} + +.tab-pane { + height: 100%; + overflow: hidden; + background-color: var(--bg-primary); +} + +/* Selection Workspace */ +.selection-workspace { + height: 100%; + display: flex; + flex-direction: column; + padding: 16px; +} + +.selection-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-primary); +} + +.section-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.selection-content { + flex: 1; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + overflow: hidden; +} + +.selection-list-panel, +.preview-panel { + display: flex; + flex-direction: column; + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px; + overflow: hidden; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background-color: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); +} + +.panel-title { + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); +} + +.selection-list, +.preview-content { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.selected-items { + list-style: none; + margin: 0; + padding: 0; +} + +.selected-items li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 2px 0; + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: 4px; + font-size: 12px; +} + +.selected-items li:hover { + background-color: var(--bg-tertiary); +} + +.empty-selection, +.empty-preview { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 150px; + color: var(--text-muted); + text-align: center; +} + +.empty-selection i, +.empty-preview i { + font-size: 20px; + margin-bottom: 8px; + opacity: 0.5; +} + +.empty-selection p, +.empty-preview p { + margin: 4px 0; + font-size: 13px; +} + +.empty-selection small { + font-size: 11px; + opacity: 0.7; +} + +.file-preview { + background-color: var(--bg-primary); + color: var(--text-primary); + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 11px; + line-height: 1.4; + white-space: pre-wrap; + padding: 12px; + overflow-y: auto; + height: 100%; +} + +/* Prompt Workspace */ +.prompt-workspace { + height: 100%; + display: grid; + grid-template-rows: 1fr 1fr; + gap: 16px; + padding: 16px; +} + +.prompt-editor, +.prompt-output { + display: flex; + flex-direction: column; + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px; + overflow: hidden; +} + +.editor-header, +.output-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background-color: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); +} + +.editor-content, +.output-content { + flex: 1; + padding: 12px; + overflow: hidden; +} + +.modern-textarea { + width: 100%; + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + font-size: 13px; + line-height: 1.5; + padding: 12px; + border-radius: 4px; + resize: vertical; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.modern-textarea:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2); + outline: none; +} + +.editor-footer { + padding: 12px; + background-color: var(--bg-tertiary); + border-top: 1px solid var(--border-primary); + display: flex; + gap: 8px; +} + +.prompt-result { + background-color: var(--bg-primary); + color: var(--text-primary); + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 11px; + line-height: 1.4; + white-space: pre-wrap; + padding: 12px; + overflow-y: auto; + height: 100%; + border: 1px solid var(--border-primary); + border-radius: 4px; +} + +.empty-output { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100px; + color: var(--text-muted); + text-align: center; +} + +.empty-output i { + font-size: 20px; + margin-bottom: 8px; + opacity: 0.5; +} + +.empty-output p { + margin: 0; + font-size: 13px; +} + +/* Chat Workspace */ +.chat-workspace { + height: 100%; + display: flex; + flex-direction: column; + padding: 16px; +} + +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-primary); +} + +.chat-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-messages { + flex: 1; + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px 6px 0 0; + overflow: hidden; +} + +.messages-container { + height: 100%; + overflow-y: auto; + padding: 16px; +} + +.welcome-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 150px; + color: var(--text-muted); + text-align: center; +} + +.welcome-message i { + font-size: 24px; + margin-bottom: 8px; + opacity: 0.5; +} + +.welcome-message p { + margin: 4px 0; + font-size: 14px; +} + +.welcome-message small { + font-size: 12px; + opacity: 0.7; +} + +.chat-input { + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-top: none; + border-radius: 0 0 6px 6px; + padding: 12px; +} + +.input-container { + display: flex; + gap: 8px; + align-items: flex-end; +} + +.chat-textarea { + flex: 1; + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + font-size: 13px; + line-height: 1.4; + padding: 8px 12px; + border-radius: 4px; + resize: none; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.chat-textarea:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2); + outline: none; +} + +.send-btn { + padding: 8px 12px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 4px; +} + +/* Button styles */ +.btn { + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + padding: 6px 12px; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.btn-primary { + background-color: var(--link-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--link-hover-color); +} + +.btn-secondary { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +.btn-secondary:hover { + background-color: var(--menu-item-hover-bg); +} + +.btn-sm { + font-size: 12px; + padding: 4px 8px; +} + +/* Loading and status states */ .loading { opacity: 0.6; pointer-events: none; + position: relative; } .loading::after { @@ -245,9 +819,9 @@ position: absolute; top: 50%; left: 50%; - width: 20px; - height: 20px; - margin: -10px 0 0 -10px; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; border: 2px solid var(--border-primary); border-top-color: var(--link-color); border-radius: 50%; @@ -260,66 +834,68 @@ } } -/* Error states */ -.error-message { - color: var(--danger-color); - background-color: rgba(220, 53, 69, 0.1); - border: 1px solid var(--danger-color); - border-radius: 0.375rem; - padding: 0.5rem; - font-size: 0.875rem; -} - -/* Success states */ .success-message { color: var(--success-color); background-color: rgba(25, 135, 84, 0.1); border: 1px solid var(--success-color); - border-radius: 0.375rem; - padding: 0.5rem; - font-size: 0.875rem; + border-radius: 4px; + padding: 8px 12px; + font-size: 12px; } -/* Responsive adjustments */ +.error-message { + color: var(--danger-color); + background-color: rgba(220, 53, 69, 0.1); + border: 1px solid var(--danger-color); + border-radius: 4px; + padding: 8px 12px; + font-size: 12px; +} + +/* Responsive design */ @media (max-width: 768px) { - .tree { - font-size: 0.8rem; + .selection-content { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; } - .tab-pane { - height: calc(100vh - 150px); + .prompt-workspace { + grid-template-rows: 1fr 1fr; } - #preview, - #promptOutput { - font-size: 0.7rem; + .tab-navigation { + flex-direction: column; + gap: 8px; + align-items: stretch; + } + + .workspace-actions { + justify-content: center; } } -/* High contrast mode support */ +/* High contrast mode */ @media (prefers-contrast: high) { - .tree .dir-label:hover, - .tree .file:hover { + .tree-item-content:hover, + .btn-ghost:hover { background-color: var(--text-primary); color: var(--bg-primary); } - .btn-outline-primary, - .btn-outline-secondary, - .btn-outline-danger { + .btn-primary, + .btn-secondary { border-width: 2px; } } -/* Reduced motion support */ +/* Reduced motion */ @media (prefers-reduced-motion: reduce) { - .tree .chev, - .tree .dir-label, - .tree .file, - .form-control, - .btn { + .tree-expand-btn, + .btn, + .modern-tabs .nav-link, + .tree-item-content { transition: none; } diff --git a/lib/web/ui/static/js/heroprompt.js b/lib/web/ui/static/js/heroprompt.js index 3a9255c0..feb53b02 100644 --- a/lib/web/ui/static/js/heroprompt.js +++ b/lib/web/ui/static/js/heroprompt.js @@ -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 = '
Loading...
'; - - const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`); - if (r.error) { - previewEl.innerHTML = `
Error: ${r.error}
`; - 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 = `
  • ${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)); + // 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 = '
    Loading workspace...
    '; - 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 = '
  • No files selected
  • '; - } 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 = '
    Loading...
    '; + + const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`); + + if (r.error) { + previewEl.innerHTML = `
    Error: ${r.error}
    `; + 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 = ` +
  • + +

    No files selected

    + Use checkboxes in the explorer to select files +
  • + `; + } 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 = ''; + 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 = '
    Loading workspace...
    '; + + const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(workspacePath)}`); + + this.container.innerHTML = ''; + + if (r.error) { + this.container.innerHTML = `
    ${r.error}
    `; + 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 = '
    No workspaces available. Create one to get started.
    '; + treeEl.innerHTML = ` +
    + +

    No workspaces available

    + Create one to get started +
    + `; } 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 = '
    No files selected. Please select files first.
    '; + return; + } + + outputEl.innerHTML = '
    Generating prompt...
    '; + + 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 = '
    Failed to generate prompt
    '; + } +} + +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 = '
    Prompt copied to clipboard!
    '; + setTimeout(() => { + outputEl.innerHTML = originalContent; + }, 2000); + } catch (e) { + console.warn('Copy failed', e); + outputEl.innerHTML = '
    Failed to copy prompt
    '; + } } // 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 = '
    Search functionality coming soon...
    '; - } - }; - } - - // 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 = '
    Prompt copied to clipboard!
    '; - setTimeout(() => { - outputEl.innerHTML = '
    Generated prompt will appear here
    '; - }, 3000); - } - } catch (e) { - console.warn('copy prompt failed', e); - const outputEl = el('promptOutput'); - if (outputEl) { - outputEl.innerHTML = '
    Failed to copy prompt
    '; - setTimeout(() => { - outputEl.innerHTML = '
    Generated prompt will appear here
    '; - }, 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 = '
    Loading workspaces...
    '; + list.innerHTML = '
    Loading workspaces...
    '; 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'); }); diff --git a/lib/web/ui/templates/heroprompt.html b/lib/web/ui/templates/heroprompt.html index db46623b..eae0a2d6 100644 --- a/lib/web/ui/templates/heroprompt.html +++ b/lib/web/ui/templates/heroprompt.html @@ -45,122 +45,262 @@
    - +
    -
    -
    -
    Workspace Explorer
    -
    - - -
    -
    -
    -
    - - -
    - -
    -
    - - +
    +
    +
    +
    Explorer
    +
    + + + +
    -
    -
    Select a workspace to browse files
    +
    +
    -
    - Click + buttons to add files/directories to selection +
    +
    + + + + + +
    +
    +
    + +
    +
    +
    +
    + + 0 selected + +
    +
    + + +
    +
    +
    + +
    +
    + +

    Select a workspace to browse files

    +
    - +
    -
    -
    - -
    -
    - -
    -
    -
    Selected Files & Directories
    - ~0 tokens +
    +
    +
    + -
    -
    -
    -
      -
    • No files selected
    • -
    +
    + + + 0 tokens + +
    +
    +
    + +
    + +
    +
    +
    +
    Selected Files & Directories
    +
    + +
    -
    -
    -
    Select a file to preview
    + +
    +
    +
    + Selected Items + +
    +
    +
      +
    • + +

      No files selected

      + Use checkboxes in the explorer to select files +
    • +
    +
    +
    + +
    +
    + File Preview +
    + +
    +
    +
    +
    +
    + +

    Select a file to preview

    +
    +
    +
    -