|
360 | 360 | // ============================================================================ |
361 | 361 | // STATE |
362 | 362 | // ============================================================================ |
363 | | - const S = { files: [], tree: [], root: 'project', ctx: '', busy: false, rendered: 0, basePath: '', basePathSet: false }; |
| 363 | + const S = { files: [], tree: [], root: 'project', ctx: '', busy: false, rendered: 0, basePath: '', basePathSet: false, model: 'gpt' }; |
364 | 364 | const $ = id => document.getElementById(id); |
365 | 365 | const D = { |
366 | 366 | side: $('sidebar'), tog: $('toggleSidebar'), tree: $('fileTree'), |
|
371 | 371 | cnt: $('fileCount'), tok: $('tokenCount'), sz: $('totalSize'), |
372 | 372 | pron: $('tokenPronunciation'), lang: $('languagesList'), |
373 | 373 | copy: $('copyBtn'), txt: $('downloadTxtBtn'), |
374 | | - load: $('loadingOverlay'), toast: $('toastContainer') |
| 374 | + load: $('loadingOverlay'), toast: $('toastContainer'), |
| 375 | + model: $('modelSelector'), |
| 376 | + loadingText: document.getElementById('loadingText') |
375 | 377 | }; |
376 | 378 | const inp = document.createElement('input'); |
377 | 379 | inp.type = 'file'; inp.webkitdirectory = true; inp.multiple = true; inp.style.display = 'none'; |
|
420 | 422 | // ============================================================================ |
421 | 423 | // UTILS |
422 | 424 | // ============================================================================ |
423 | | - const load = s => (D.load.classList.toggle('active', s), S.busy = s); |
| 425 | + // Enhanced load function with optional message |
| 426 | + const load = (s, msg) => { |
| 427 | + D.load.classList.toggle('active', s); |
| 428 | + S.busy = s; |
| 429 | + if (D.loadingText && msg) D.loadingText.textContent = msg; |
| 430 | + else if (D.loadingText && s) D.loadingText.textContent = 'Processing files...'; |
| 431 | + }; |
424 | 432 | const toast = (m, t = 'info') => { |
425 | 433 | const el = document.createElement('div'); |
426 | 434 | el.className = `toast ${t}`; |
|
1041 | 1049 | if (S.busy) return; |
1042 | 1050 | const cbs = Array.from(document.querySelectorAll('.file-checkbox:checked:not([disabled])')); |
1043 | 1051 | if (!cbs.length) { toast('Select files first', 'warning'); return; } |
1044 | | - load(true); |
| 1052 | + load(true, 'Generating context...'); |
1045 | 1053 | D.ed.innerHTML = '<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Preparing...</p></div>'; |
1046 | 1054 |
|
| 1055 | + // Get selected model |
| 1056 | + S.model = D.model ? D.model.value : 'gpt'; |
| 1057 | + |
1047 | 1058 | try { |
1048 | 1059 | const paths = cbs.map(c => c.dataset.path); |
1049 | 1060 | // Ensure we only use files that were originally loaded (and thus already filtered) |
|
1118 | 1129 | const content = result.value; |
1119 | 1130 | const path = batch[idx].path; |
1120 | 1131 | const filename = batch[idx].name; |
1121 | | - |
1122 | 1132 | // Validate content is actually text (not binary disguised as text) |
1123 | | - // Check for null bytes or high ratio of non-printable characters |
1124 | 1133 | if (content.indexOf('\0') !== -1) { |
1125 | 1134 | console.warn('Skipping file with null bytes (binary):', filename); |
1126 | 1135 | failed++; |
1127 | 1136 | return; |
1128 | 1137 | } |
1129 | | - |
1130 | 1138 | // Skip if content looks like base64 encoded data (common in compiled files) |
1131 | 1139 | const lines = content.split('\n'); |
1132 | 1140 | const longBase64Lines = lines.filter(line => |
1133 | 1141 | line.length > 200 && /^[A-Za-z0-9+/=]+$/.test(line.trim()) |
1134 | 1142 | ).length; |
1135 | | - |
1136 | 1143 | if (longBase64Lines > 10) { |
1137 | 1144 | console.warn('Skipping file with encoded data:', filename); |
1138 | 1145 | failed++; |
1139 | 1146 | return; |
1140 | 1147 | } |
1141 | | - |
1142 | 1148 | // Skip extremely long single lines (typical of minified code) |
1143 | 1149 | const hasVeryLongLine = lines.some(line => line.length > 10000); |
1144 | 1150 | if (hasVeryLongLine) { |
1145 | 1151 | console.warn('Skipping file with very long lines (minified):', filename); |
1146 | 1152 | failed++; |
1147 | 1153 | return; |
1148 | 1154 | } |
1149 | | - |
1150 | 1155 | contents.push({ |
1151 | 1156 | path: path, |
1152 | 1157 | content: content |
|
1160 | 1165 | const pct = Math.round(done / textFiles.length * 100); |
1161 | 1166 | const status = []; |
1162 | 1167 | status.push(`<strong>${done}/${textFiles.length}</strong> files`); |
1163 | | - if (skipped > 0) status.push(`<span style="color:#3b82f6">${skipped} binary</span>`); |
1164 | | - if (failed > 0) status.push(`<span style="color:#fbbf24">${failed} failed</span>`); |
1165 | | - D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Reading files: ${pct}%</p><small>${status.join(' • ')}</small></div>`; |
| 1168 | + if (skipped > 0) status.push(`<span style=\"color:#3b82f6\">${skipped} binary</span>`); |
| 1169 | + if (failed > 0) status.push(`<span style=\"color:#fbbf24\">${failed} failed</span>`); |
| 1170 | + D.ed.innerHTML = `<div class=\"editor-placeholder\"><span class=\"material-symbols-outlined\">hourglass_empty</span><p>Reading files: ${pct}%</p><small>${status.join(' • ')}</small></div>`; |
| 1171 | + load(true, `Reading files: ${pct}%`); |
1166 | 1172 | // Yield to UI only every 5 batches for better performance |
1167 | 1173 | if (i % (FAST_BATCH * 5) === 0) { |
1168 | 1174 | await new Promise(r => setTimeout(r, 0)); |
1169 | 1175 | } |
1170 | 1176 | } |
1171 | 1177 |
|
1172 | 1178 | D.ed.innerHTML = '<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Building output...</p></div>'; |
| 1179 | + load(true, 'Building output...'); |
1173 | 1180 | await new Promise(r => setTimeout(r, 10)); |
1174 | 1181 | const parts = []; |
| 1182 | + // === AI Model Template Logic === |
| 1183 | + if (S.model === 'gpt') { |
| 1184 | + parts.push('### GPT-4 CONTEXT TEMPLATE\n'); |
| 1185 | + } else if (S.model === 'claude') { |
| 1186 | + parts.push('### CLAUDE CONTEXT TEMPLATE\n'); |
| 1187 | + } else if (S.model === 'gemini') { |
| 1188 | + parts.push('### GEMINI CONTEXT TEMPLATE\n'); |
| 1189 | + } |
1175 | 1190 | parts.push('<folder-structure>\n', struct, '</folder-structure>\n'); |
1176 | 1191 | // Binary files are intentionally excluded from the generated output (they remain visible in the tree) |
1177 | 1192 | // Build document sections faster - no UI updates during build |
1178 | 1193 | contents.forEach(({ path, content }) => { |
1179 | 1194 | const full = getFullPath(path); |
1180 | 1195 | parts.push(`=== FILE: ${full} ===\n`); |
1181 | | - parts.push(`<document path="${path}">\n`, content, '\n</document>\n'); |
| 1196 | + parts.push(`<document path=\"${path}\">\n`, content, '\n</document>\n'); |
1182 | 1197 | }); |
1183 | 1198 |
|
1184 | 1199 | let ctx; |
1185 | 1200 | try { |
1186 | 1201 | ctx = parts.join(''); // This is now only done for smaller projects |
1187 | 1202 | } catch (e) { |
1188 | 1203 | console.error('Too large for string join (unexpected):', e); |
1189 | | - D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">warning</span><h3 style="color:#fbbf24;margin:10px 0">Too Large!</h3><p>Size: <strong>${bytes(parts.reduce((s, p) => s + p.length, 0))}</strong></p><p>Use download button.</p></div>`; |
| 1204 | + D.ed.innerHTML = `<div class=\"editor-placeholder\"><span class=\"material-symbols-outlined\">warning</span><h3 style=\"color:#fbbf24;margin:10px 0\">Too Large!</h3><p>Size: <strong>${bytes(parts.reduce((s, p) => s + p.length, 0))}</strong></p><p>Use download button.</p></div>`; |
1190 | 1205 | S.ctx = parts; |
1191 | 1206 | S.isArray = true; |
1192 | 1207 | toast('Ready for download!', 'success'); |
|
1204 | 1219 | if (isLarge) { |
1205 | 1220 | const prev = ctx.substring(0, 50000); |
1206 | 1221 | const rem = ctx.length - 50000; |
1207 | | - D.ed.innerHTML = `<div style="padding:12px;background:#1e1e1e;border-radius:4px;"><div style="background:#2d2d30;padding:8px;border-radius:4px;margin-bottom:12px;border-left:3px solid #fbbf24;"><strong style="color:#fbbf24;">⚠️ Preview</strong><br><small style="color:#888;">First ${bytes(50000)} of ${bytes(ctx.length)}</small><br><small style="color:#888;">Use download for full</small></div><pre style="margin:0;white-space:pre-wrap;word-wrap:break-word;font-size:11px;line-height:1.3;max-height:500px;overflow:auto">${esc(prev)}</pre><div style="background:#2d2d30;padding:8px;border-radius:4px;margin-top:12px;text-align:center;"><small style="color:#888;">... ${bytes(rem)} more</small></div></div>`; |
| 1222 | + D.ed.innerHTML = `<div style=\"padding:12px;background:#1e1e1e;border-radius:4px;\"><div style=\"background:#2d2d30;padding:8px;border-radius:4px;margin-bottom:12px;border-left:3px solid #fbbf24;\"><strong style=\"color:#fbbf24;\">⚠️ Preview</strong><br><small style=\"color:#888;\">First ${bytes(50000)} of ${bytes(ctx.length)}</small><br><small style=\"color:#888;\">Use download for full</small></div><pre style=\"margin:0;white-space:pre-wrap;word-wrap:break-word;font-size:11px;line-height:1.3;max-height:500px;overflow:auto\">${esc(prev)}</pre><div style=\"background:#2d2d30;padding:8px;border-radius:4px;margin-top:12px;text-align:center;\"><small style=\"color:#888;\">... ${bytes(rem)} more</small></div></div>`; |
1208 | 1223 | } else { |
1209 | | - D.ed.innerHTML = `<pre style="margin:0;padding:12px;white-space:pre-wrap;word-wrap:break-word;font-size:11px;line-height:1.3;max-height:100%;overflow:auto">${esc(ctx)}</pre>`; |
| 1224 | + D.ed.innerHTML = `<pre style=\"margin:0;padding:12px;white-space:pre-wrap;word-wrap:break-word;font-size:11px;line-height:1.3;max-height:100%;overflow:auto\">${esc(ctx)}</pre>`; |
1210 | 1225 | } |
1211 | 1226 | const stats = []; |
1212 | 1227 | stats.push(`${textFiles.length} files`); |
|
1220 | 1235 | } catch (e) { |
1221 | 1236 | console.error('Gen error:', e); |
1222 | 1237 | toast('Failed: ' + e.message, 'error'); |
1223 | | - D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">error</span><p>Failed</p><small style="color:#ef4444">${e.message}</small></div>`; |
| 1238 | + D.ed.innerHTML = `<div class=\"editor-placeholder\"><span class=\"material-symbols-outlined\">error</span><p>Failed</p><small style=\"color:#ef4444\">${e.message}</small></div>`; |
1224 | 1239 | } finally { |
1225 | 1240 | load(false); |
1226 | 1241 | } |
|
1241 | 1256 | if (result.status === 'fulfilled') { |
1242 | 1257 | const content = result.value; |
1243 | 1258 | const p = batch[idx].path; |
1244 | | - const filename = batch[idx].name; |
1245 | | - |
1246 | | - // Same validation as in gen() function |
1247 | | - // Skip files with null bytes (binary disguised as text) |
1248 | | - if (content.indexOf('\0') !== -1) { |
1249 | | - console.warn('Download: Skipping file with null bytes:', filename); |
1250 | | - return; |
1251 | | - } |
1252 | | - |
1253 | | - // Skip base64 encoded content |
1254 | | - const lines = content.split('\n'); |
1255 | | - const longBase64Lines = lines.filter(line => |
1256 | | - line.length > 200 && /^[A-Za-z0-9+/=]+$/.test(line.trim()) |
1257 | | - ).length; |
1258 | | - |
1259 | | - if (longBase64Lines > 10) { |
1260 | | - console.warn('Download: Skipping file with encoded data:', filename); |
1261 | | - return; |
1262 | | - } |
1263 | | - |
1264 | | - // Skip minified files (very long single lines) |
1265 | | - const hasVeryLongLine = lines.some(line => line.length > 10000); |
1266 | | - if (hasVeryLongLine) { |
1267 | | - console.warn('Download: Skipping minified file:', filename); |
1268 | | - return; |
1269 | | - } |
1270 | | - |
1271 | | - parts.push('=== FILE: ' + getFullPath(p) + ' ===\n'); |
1272 | | - parts.push('<document path="' + p + '">\n', content, '\n</document>\n'); |
1273 | | - |
1274 | | - // OPTIMIZE: Nullify the file reference from S.files after reading to help GC |
1275 | | - const sFileIndex = S.files.findIndex(sf => sf.path === p); |
1276 | | - if (sFileIndex !== -1) { |
1277 | | - S.files[sFileIndex].file = null; // Release the File object reference |
1278 | | - } |
1279 | | - } else { |
1280 | | - console.warn('Skip', batch[idx].name, result.reason); |
1281 | | - } |
1282 | | - }); |
1283 | | - done += batch.length; |
1284 | | - const pct = Math.round(done / textFiles.length * 100); |
1285 | | - const status = []; |
1286 | | - status.push(`${done}/${textFiles.length}`); |
1287 | | - if (binaryFiles && binaryFiles.length > 0) { |
1288 | | - status.push(`${binaryFiles.length} binary`); |
1289 | | - } |
1290 | | - D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">download</span><p>Preparing: ${pct}%</p><small>${status.join(' • ')}</small></div>`; |
| 1259 | + const loadFiles = async list => { |
| 1260 | + try { |
| 1261 | + // Clear previous memory before loading new files |
| 1262 | + clearMemory(); |
1291 | 1263 |
|
1292 | | - // OPTIMIZE: Yield control frequently during parts building |
1293 | | - if (i % (FAST_BATCH * 5) === 0) { // Yield every 5 batches |
1294 | | - await new Promise(r => setTimeout(r, 0)); |
1295 | | - } |
1296 | | - } |
1297 | | - |
1298 | | - // OPTIMIZE: Yield control one final time before creating Blob |
1299 | | - await new Promise(r => setTimeout(r, 0)); |
1300 | | - |
1301 | | - const blob = new Blob(parts, { type: 'text/plain' }); |
1302 | | - const url = URL.createObjectURL(blob); |
1303 | | - const a = document.createElement('a'); |
1304 | | - a.href = url; |
1305 | | - a.download = `${S.root}-context.txt`; |
1306 | | - a.click(); |
1307 | | - URL.revokeObjectURL(url); // REVOKE URL IMMEDIATELY |
1308 | | - |
1309 | | - const stats = []; |
1310 | | - stats.push(`${textFiles.length} files`); |
1311 | | - if (binaryFiles && binaryFiles.length > 0) { |
1312 | | - stats.push(`${binaryFiles.length} binary`); |
1313 | | - } |
1314 | | - stats.push(bytes(blob.size)); |
1315 | | - D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">check_circle</span><p>Downloaded!</p><small>File: ${S.root}-context.txt</small><small>${stats.join(' • ')}</small></div>`; |
1316 | | - |
1317 | | - // Explicitly clear parts array after download |
1318 | | - parts.length = 0; |
1319 | | - }; |
| 1264 | + load(true, `Loading ${list.length} files...`); |
| 1265 | + // Fast path: convert FileList to Array |
| 1266 | + const all = Array.from(list); |
| 1267 | + load(true, 'Filtering files...'); |
| 1268 | + await new Promise(r => setTimeout(r, 0)); |
| 1269 | + // Filter files - CRITICAL: Ensure all ignored files are removed here |
| 1270 | + S.files = all.filter(f => !ign(f.webkitRelativePath)) |
| 1271 | + .map(f => ({ |
| 1272 | + path: f.webkitRelativePath, |
| 1273 | + name: f.name, |
| 1274 | + size: f.size, |
| 1275 | + file: f |
| 1276 | + })); |
| 1277 | + // Auto-detect absolute base path when running in desktop/Electron environments |
| 1278 | + // where File objects may expose a non-standard `path` property. |
| 1279 | + if (!S.basePathSet) { |
| 1280 | + const cand = S.files.find(ff => ff.file && ff.file.path); |
| 1281 | + if (cand && cand.file && cand.file.path) { |
| 1282 | + try { |
| 1283 | + const full = String(cand.file.path); |
| 1284 | + const rel = String(cand.path); |
| 1285 | + const sep = full.indexOf('\\') !== -1 ? '\\' : '/'; |
| 1286 | + const relConv = rel.split('/').join(sep); |
| 1287 | + let base = ''; |
| 1288 | + if (full.endsWith(relConv)) { |
| 1289 | + base = full.slice(0, full.length - relConv.length); |
| 1290 | + base = base.replace(/[\\/]+$/, ''); |
| 1291 | + } else { |
| 1292 | + const idx = full.indexOf(relConv); |
| 1293 | + if (idx !== -1) base = full.slice(0, idx).replace(/[\\/]+$/, ''); |
| 1294 | + } |
| 1295 | + if (base) { |
| 1296 | + S.basePath = base; |
| 1297 | + S.basePathSet = true; |
| 1298 | + console.info('Auto-detected base path:', base); |
| 1299 | + } |
| 1300 | + } catch (e) { |
| 1301 | + // ignore; leave base path unset |
| 1302 | + } |
| 1303 | + } |
| 1304 | + } |
| 1305 | + if (S.files.length === 0) { toast('No valid files', 'warning'); load(false); return; } |
| 1306 | + if (S.files.length > 5000) { toast(`Large directory (${S.files.length} files) - rendering first 1000`, 'warning'); } |
| 1307 | + load(true, 'Building file tree...'); |
| 1308 | + S.root = S.files[0].path.split('/')[0]; |
| 1309 | + // Let UI update before building tree |
| 1310 | + await new Promise(r => setTimeout(r, 0)); |
| 1311 | + S.tree = build(S.files.slice(0, 3000)); // Limit tree size, but build from fully filtered list |
| 1312 | + load(true, 'Rendering tree...'); |
| 1313 | + await new Promise(r => setTimeout(r, 0)); |
| 1314 | + // Render tree |
| 1315 | + const items = render(S.tree); |
| 1316 | + D.tree.innerHTML = items.map(x => x.html).join(''); |
| 1317 | + S.rendered = items.length; |
| 1318 | + stats(); |
| 1319 | + toast(`Loaded ${S.files.length} files`, 'success'); |
| 1320 | + } catch (e) { |
| 1321 | + console.error('Load error:', e); |
| 1322 | + toast('Load failed', 'error'); |
| 1323 | + } finally { |
| 1324 | + load(false); |
| 1325 | + } |
| 1326 | + }; |
| 1327 | + // (removed orphaned code) |
1320 | 1328 |
|
1321 | 1329 | const genStruct = (nodes, pfx = '') => { |
1322 | 1330 | let r = ''; |
|
1392 | 1400 | const setup = () => { |
1393 | 1401 | D.tog.onclick = () => D.side.classList.toggle('collapsed'); |
1394 | 1402 | D.sel.onclick = () => inp.click(); |
| 1403 | + // Model selector event |
| 1404 | + if (D.model) { |
| 1405 | + D.model.onchange = () => { |
| 1406 | + S.model = D.model.value; |
| 1407 | + }; |
| 1408 | + } |
1395 | 1409 | // Optimize file input change handler |
1396 | 1410 | inp.onchange = e => { |
1397 | 1411 | if (!e.target.files.length) return; |
1398 | 1412 | // Show loader IMMEDIATELY |
1399 | | - load(true); |
| 1413 | + load(true, 'Loading files...'); |
1400 | 1414 | // Process files in next tick to let loader render |
1401 | 1415 | setTimeout(() => loadFiles(e.target.files), 0); |
1402 | 1416 | }; |
|
0 commit comments