1// static/js/rag.js 2 3/** 4 * RAG (Retrieval Augmented Generation) management 5 */ 6 7import uiModule from './ui.js'; 8import spinnerModule from './spinner.js'; 9 10let API_BASE = ''; 11 12export function init(apiBase) { 13 API_BASE = apiBase; 14 _setupUploadZone(); 15} 16 17function _humanSize(bytes) { 18 if (bytes < 1024) return bytes + ' B'; 19 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; 20 return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; 21} 22 23/** 24 * Load and display RAG documents with delete buttons 25 */ 26export async function loadPersonalDocs() { 27 const box = document.getElementById('docs-view'); 28 if (!box) return; 29 30 box.innerHTML = ''; 31 const { element: wpEl } = spinnerModule.createWhirlpool(24); 32 wpEl.title = 'Loadingâ¦'; 33 box.appendChild(wpEl); 34 35 try { 36 const res = await fetch(`${API_BASE}/api/personal`, { credentials: 'same-origin' }); 37 const data = await res.json(); 38 const files = data.files || []; 39 40 box.innerHTML = ''; 41 42 if (files.length === 0) { 43 const placeholder = document.createElement('div'); 44 placeholder.textContent = 'Drop files above to add to RAG'; 45 placeholder.style.cssText = 'color:var(--color-muted);font-size:12px;padding:4px 0;'; 46 box.appendChild(placeholder); 47 return; 48 } 49 50 files.forEach(f => { 51 const row = document.createElement('div'); 52 row.className = 'list-item'; 53 row.style.cssText = 'display:flex;align-items:center;gap:4px;'; 54 55 const name = document.createElement('span'); 56 name.className = 'grow'; 57 name.textContent = f.name.split('/').pop(); 58 name.title = f.path || f.name; 59 name.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'; 60 row.appendChild(name); 61 62 const size = document.createElement('span'); 63 size.style.cssText = 'color:var(--color-muted);font-size:11px;flex-shrink:0;'; 64 size.textContent = _humanSize(f.size); 65 row.appendChild(size); 66 67 const del = document.createElement('button'); 68 del.className = 'rag-file-delete'; 69 del.textContent = 'x'; 70 del.title = 'Remove from RAG'; 71 del.style.cssText = 'background:none;border:none;color:var(--color-error);cursor:pointer;padding:2px 4px;font-size:12px;flex-shrink:0;'; 72 del.addEventListener('click', (e) => { 73 e.stopPropagation(); 74 _deleteFile(f.path || f.name, f.name.split('/').pop()); 75 }); 76 row.appendChild(del); 77 78 box.appendChild(row); 79 }); 80 } catch (e) { 81 console.error(e); 82 box.innerHTML = ''; 83 const error = document.createElement('div'); 84 error.textContent = 'Failed to load files'; 85 error.style.color = 'var(--color-error)'; 86 box.appendChild(error); 87 } 88} 89 90async function _deleteFile(filepath, displayName) { 91 if (!await uiModule.styledConfirm(`Remove "${displayName}" from RAG?`, { confirmText: 'Remove', danger: true })) return; 92 try { 93 const res = await fetch(`${API_BASE}/api/personal/file?filepath=${encodeURIComponent(filepath)}`, { 94 method: 'DELETE', 95 credentials: 'same-origin' 96 }); 97 if (!res.ok) throw new Error(await res.text()); 98 await loadPersonalDocs(); 99 } catch (e) { 100 console.error('Delete failed:', e); 101 alert('Failed to delete file: ' + e.message); 102 } 103} 104 105/** 106 * Upload files to RAG 107 */ 108export async function uploadRagFiles(fileList) { 109 if (!fileList || !fileList.length) return; 110 111 const zone = document.getElementById('rag-upload-zone'); 112 if (zone) zone.textContent = 'Uploadingâ¦'; 113 114 const fd = new FormData(); 115 for (const file of fileList) { 116 fd.append('files', file); 117 } 118 119 try { 120 const res = await fetch(`${API_BASE}/api/personal/upload`, { 121 method: 'POST', 122 credentials: 'same-origin', 123 body: fd 124 }); 125 126 if (!res.ok) throw new Error(await res.text()); 127 128 const data = await res.json(); 129 if (zone) zone.textContent = 'Drop files here or click to upload'; 130 await loadPersonalDocs(); 131 return data; 132 } catch (e) { 133 console.error('Upload failed:', e); 134 if (zone) zone.textContent = 'Drop files here or click to upload'; 135 alert('Upload failed: ' + e.message); 136 } 137} 138 139function _setupUploadZone() { 140 const zone = document.getElementById('rag-upload-zone'); 141 const input = document.getElementById('rag-file-input'); 142 if (!zone || !input) return; 143 144 zone.addEventListener('click', () => input.click()); 145 146 zone.addEventListener('dragover', (e) => { 147 e.preventDefault(); 148 zone.classList.add('dragover'); 149 }); 150 151 zone.addEventListener('dragleave', () => { 152 zone.classList.remove('dragover'); 153 }); 154 155 zone.addEventListener('drop', (e) => { 156 e.preventDefault(); 157 zone.classList.remove('dragover'); 158 if (e.dataTransfer.files.length) { 159 uploadRagFiles(e.dataTransfer.files); 160 } 161 }); 162 163 input.addEventListener('change', () => { 164 if (input.files.length) { 165 uploadRagFiles(input.files); 166 input.value = ''; 167 } 168 }); 169} 170 171const ragModule = { 172 init, 173 loadPersonalDocs, 174 uploadRagFiles 175}; 176 177export default ragModule;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.