Skip to content

Commit 7c20215

Browse files
committed
feat: Add model selection and enhance loading messages for improved user experience
1 parent 1c6d47c commit 7c20215

File tree

1 file changed

+108
-94
lines changed

1 file changed

+108
-94
lines changed

index.js

Lines changed: 108 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@
360360
// ============================================================================
361361
// STATE
362362
// ============================================================================
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' };
364364
const $ = id => document.getElementById(id);
365365
const D = {
366366
side: $('sidebar'), tog: $('toggleSidebar'), tree: $('fileTree'),
@@ -371,7 +371,9 @@
371371
cnt: $('fileCount'), tok: $('tokenCount'), sz: $('totalSize'),
372372
pron: $('tokenPronunciation'), lang: $('languagesList'),
373373
copy: $('copyBtn'), txt: $('downloadTxtBtn'),
374-
load: $('loadingOverlay'), toast: $('toastContainer')
374+
load: $('loadingOverlay'), toast: $('toastContainer'),
375+
model: $('modelSelector'),
376+
loadingText: document.getElementById('loadingText')
375377
};
376378
const inp = document.createElement('input');
377379
inp.type = 'file'; inp.webkitdirectory = true; inp.multiple = true; inp.style.display = 'none';
@@ -420,7 +422,13 @@
420422
// ============================================================================
421423
// UTILS
422424
// ============================================================================
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+
};
424432
const toast = (m, t = 'info') => {
425433
const el = document.createElement('div');
426434
el.className = `toast ${t}`;
@@ -1041,9 +1049,12 @@
10411049
if (S.busy) return;
10421050
const cbs = Array.from(document.querySelectorAll('.file-checkbox:checked:not([disabled])'));
10431051
if (!cbs.length) { toast('Select files first', 'warning'); return; }
1044-
load(true);
1052+
load(true, 'Generating context...');
10451053
D.ed.innerHTML = '<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Preparing...</p></div>';
10461054

1055+
// Get selected model
1056+
S.model = D.model ? D.model.value : 'gpt';
1057+
10471058
try {
10481059
const paths = cbs.map(c => c.dataset.path);
10491060
// Ensure we only use files that were originally loaded (and thus already filtered)
@@ -1118,35 +1129,29 @@
11181129
const content = result.value;
11191130
const path = batch[idx].path;
11201131
const filename = batch[idx].name;
1121-
11221132
// Validate content is actually text (not binary disguised as text)
1123-
// Check for null bytes or high ratio of non-printable characters
11241133
if (content.indexOf('\0') !== -1) {
11251134
console.warn('Skipping file with null bytes (binary):', filename);
11261135
failed++;
11271136
return;
11281137
}
1129-
11301138
// Skip if content looks like base64 encoded data (common in compiled files)
11311139
const lines = content.split('\n');
11321140
const longBase64Lines = lines.filter(line =>
11331141
line.length > 200 && /^[A-Za-z0-9+/=]+$/.test(line.trim())
11341142
).length;
1135-
11361143
if (longBase64Lines > 10) {
11371144
console.warn('Skipping file with encoded data:', filename);
11381145
failed++;
11391146
return;
11401147
}
1141-
11421148
// Skip extremely long single lines (typical of minified code)
11431149
const hasVeryLongLine = lines.some(line => line.length > 10000);
11441150
if (hasVeryLongLine) {
11451151
console.warn('Skipping file with very long lines (minified):', filename);
11461152
failed++;
11471153
return;
11481154
}
1149-
11501155
contents.push({
11511156
path: path,
11521157
content: content
@@ -1160,33 +1165,43 @@
11601165
const pct = Math.round(done / textFiles.length * 100);
11611166
const status = [];
11621167
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}%`);
11661172
// Yield to UI only every 5 batches for better performance
11671173
if (i % (FAST_BATCH * 5) === 0) {
11681174
await new Promise(r => setTimeout(r, 0));
11691175
}
11701176
}
11711177

11721178
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...');
11731180
await new Promise(r => setTimeout(r, 10));
11741181
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+
}
11751190
parts.push('<folder-structure>\n', struct, '</folder-structure>\n');
11761191
// Binary files are intentionally excluded from the generated output (they remain visible in the tree)
11771192
// Build document sections faster - no UI updates during build
11781193
contents.forEach(({ path, content }) => {
11791194
const full = getFullPath(path);
11801195
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');
11821197
});
11831198

11841199
let ctx;
11851200
try {
11861201
ctx = parts.join(''); // This is now only done for smaller projects
11871202
} catch (e) {
11881203
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>`;
11901205
S.ctx = parts;
11911206
S.isArray = true;
11921207
toast('Ready for download!', 'success');
@@ -1204,9 +1219,9 @@
12041219
if (isLarge) {
12051220
const prev = ctx.substring(0, 50000);
12061221
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>`;
12081223
} 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>`;
12101225
}
12111226
const stats = [];
12121227
stats.push(`${textFiles.length} files`);
@@ -1220,7 +1235,7 @@
12201235
} catch (e) {
12211236
console.error('Gen error:', e);
12221237
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>`;
12241239
} finally {
12251240
load(false);
12261241
}
@@ -1241,82 +1256,75 @@
12411256
if (result.status === 'fulfilled') {
12421257
const content = result.value;
12431258
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();
12911263

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)
13201328

13211329
const genStruct = (nodes, pfx = '') => {
13221330
let r = '';
@@ -1392,11 +1400,17 @@
13921400
const setup = () => {
13931401
D.tog.onclick = () => D.side.classList.toggle('collapsed');
13941402
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+
}
13951409
// Optimize file input change handler
13961410
inp.onchange = e => {
13971411
if (!e.target.files.length) return;
13981412
// Show loader IMMEDIATELY
1399-
load(true);
1413+
load(true, 'Loading files...');
14001414
// Process files in next tick to let loader render
14011415
setTimeout(() => loadFiles(e.target.files), 0);
14021416
};

0 commit comments

Comments
 (0)