Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-04-24 - Pre-calculate derived search properties outside the render loop
**Learning:** In list rendering algorithms, calculating derived data (like full search strings and formatted dates) during the render or filter loops introduces significant unnecessary CPU overhead. The application was previously re-instantiating `Date` objects, calling `.toLocaleDateString()`, and concatenating multiple strings for every PDF item during every keystroke search and re-render.
**Action:** Always pre-calculate derived search properties (`_searchStr`, `_isNew`, `_formattedDate`) during the initial data load into the database/state. Use `Intl.DateTimeFormat` which is significantly faster than `.toLocaleDateString()`. Store this derived state on the items but ensure it is added *after* the core data is cached to `localStorage` to avoid cache bloat. Utilize early returns (`if (!condition) return false;`) in the filter loop to speed up traversal.
35 changes: 35 additions & 0 deletions patch_prepare.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<<<<<<< SEARCH
async function loadPDFDatabase() {
=======
function prepareSearchIndex(data) {
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
const now = new Date().getTime();
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;

for (let i = 0; i < data.length; i++) {
const pdf = data[i];

// 1. Precalculate search string
pdf._searchStr = (
(pdf.title || '') + ' ' +
(pdf.description || '') + ' ' +
(pdf.category || '') + ' ' +
(pdf.author || '')
).toLowerCase();

// 2. Precalculate date logic
const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj)) {
pdf._formattedDate = dateFormatter.format(uploadDateObj);
pdf._isNew = (now - uploadDateObj.getTime()) < SEVEN_DAYS;
} else {
pdf._formattedDate = 'Unknown Date';
pdf._isNew = false;
}
}
}

async function loadPDFDatabase() {
>>>>>>> REPLACE
6 changes: 6 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
1. **Add `prepareSearchIndex` function in `script.js`**: Create a helper function to precalculate `_searchStr`, `_isNew`, and `_formattedDate` for all PDF objects. This will use an `Intl.DateTimeFormat` instance for optimal date formatting performance and validate timestamps using `!isNaN()`.
2. **Call `prepareSearchIndex` after data load**: Update `loadPDFDatabase` to call `prepareSearchIndex(pdfDatabase)` immediately after loading from cache or fetching from Firebase (after `localStorage.setItem` to avoid bloating the cache).
3. **Optimize `renderPDFs` filter**: Update the filter logic in `renderPDFs` to use early returns (`if (!condition) return false;`) and use the pre-calculated `_searchStr` with a truthiness guard instead of rebuilding the search string on every keypress.
4. **Update `createPDFCard`**: Update `createPDFCard` to use the pre-calculated `_isNew` and `_formattedDate` values, keeping inline calculation fallbacks for unindexed data (e.g. `if (isNew === undefined) { ... }`).
5. **Complete pre commit steps**: Ensure proper testing, verification, review, and reflection are done.
6. **Submit PR**: Commit and submit the code with a "⚑ Bolt: [performance improvement]" PR title and detailed description.
80 changes: 60 additions & 20 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,36 @@ async function syncClassSwitcher() {
renderSemesterTabs();
}

function prepareSearchIndex(data) {
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
const now = new Date().getTime();
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;

for (let i = 0; i < data.length; i++) {
const pdf = data[i];

// 1. Precalculate search string
pdf._searchStr = (
(pdf.title || '') + ' ' +
(pdf.description || '') + ' ' +
(pdf.category || '') + ' ' +
(pdf.author || '')
).toLowerCase();

// 2. Precalculate date logic
const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj)) {
pdf._formattedDate = dateFormatter.format(uploadDateObj);
pdf._isNew = (now - uploadDateObj.getTime()) < SEVEN_DAYS;
} else {
pdf._formattedDate = 'Unknown Date';
pdf._isNew = false;
}
}
}

async function loadPDFDatabase() {
if (isMaintenanceActive) return;

Expand Down Expand Up @@ -490,6 +520,7 @@ async function loadPDFDatabase() {

if (shouldUseCache) {
pdfDatabase = cachedData;
prepareSearchIndex(pdfDatabase);
// --- FIX: CALL THIS TO POPULATE UI ---
syncClassSwitcher();
renderSemesterTabs();
Expand All @@ -513,6 +544,8 @@ async function loadPDFDatabase() {
data: pdfDatabase
}));

prepareSearchIndex(pdfDatabase);

// --- FIX: CALL THIS TO POPULATE UI ---
syncClassSwitcher();
renderPDFs();
Expand Down Expand Up @@ -948,26 +981,24 @@ function renderPDFs() {

// Locate renderPDFs() in script.js and update the filter section
const filteredPdfs = pdfDatabase.filter(pdf => {
const matchesSemester = pdf.semester === currentSemester;
if (pdf.semester !== currentSemester) return false;

// NEW: Check if the PDF class matches the UI's current class selection
// Note: If old documents don't have this field, they will be hidden.
const matchesClass = pdf.class === currentClass;
if (pdf.class !== currentClass) return false;

let matchesCategory = false;
if (currentCategory === 'favorites') {
matchesCategory = favorites.includes(pdf.id);
} else {
matchesCategory = currentCategory === 'all' || pdf.category === currentCategory;
if (!favorites.includes(pdf.id)) return false;
} else if (currentCategory !== 'all' && pdf.category !== currentCategory) {
return false;
}

const matchesSearch = pdf.title.toLowerCase().includes(searchTerm) ||
pdf.description.toLowerCase().includes(searchTerm) ||
pdf.category.toLowerCase().includes(searchTerm) ||
pdf.author.toLowerCase().includes(searchTerm);
if (searchTerm) {
if (!pdf._searchStr) return false; // Safety for unindexed items
if (!pdf._searchStr.includes(searchTerm)) return false;
}

// Update return statement to include matchesClass
return matchesSemester && matchesClass && matchesCategory && matchesSearch;
return true;
});

updatePDFCount(filteredPdfs.length);
Expand Down Expand Up @@ -1037,9 +1068,23 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) {
const heartIconClass = isFav ? 'fas' : 'far';
const btnActiveClass = isFav ? 'active' : '';

const uploadDateObj = new Date(pdf.uploadDate);
const timeDiff = new Date() - uploadDateObj;
const isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days
let isNew = pdf._isNew;
let formattedDate = pdf._formattedDate;

// Fallback for items that weren't indexed (e.g., from old cache)
if (isNew === undefined || !formattedDate) {
const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj)) {
const timeDiff = new Date() - uploadDateObj;
isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days
formattedDate = uploadDateObj.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
} else {
isNew = false;
formattedDate = 'Unknown Date';
}
}

const newBadgeHTML = isNew
? `<span style="background:var(--error-color); color:white; font-size:0.6rem; padding:2px 6px; border-radius:4px; margin-left:8px; vertical-align:middle;">NEW</span>`
Expand All @@ -1053,11 +1098,6 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) {
};
const categoryIcon = categoryIcons[pdf.category] || 'fa-file-pdf';

// Formatting Date
const formattedDate = new Date(pdf.uploadDate).toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});

// Uses global escapeHtml() now

const highlightText = (text) => {
Expand Down
2 changes: 2 additions & 0 deletions test_perf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const fs = require('fs');
let code = fs.readFileSync('script.js', 'utf8');
193 changes: 193 additions & 0 deletions test_playwright.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import asyncio
from playwright.async_api import async_playwright

async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()

# We will directly read the script and evaluate it to avoid loading dependencies or the full HTML
with open('script.js', 'r') as f:
script_content = f.read()

await page.set_content("""
<html>
<body>
<input id="searchInput" type="text" />
<div id="pdfGrid"></div>
<div id="pdfCount"></div>
<div id="emptyState" style="display:none"></div>
</body>
</html>
""")

# We define only the parts we need
await page.evaluate("""
window.pdfDatabase = [];
window.currentClass = 'MSc Chemistry';
window.currentSemester = 1;
window.currentCategory = 'all';
window.pdfCount = document.getElementById('pdfCount');
window.pdfGrid = document.getElementById('pdfGrid');
window.emptyState = document.getElementById('emptyState');
window.searchInput = document.getElementById('searchInput');
window.searchTimeout = null;
window.GAS_URL = '';

window.getFavorites = () => [];
window.updatePDFCount = (c) => window.pdfCount.textContent = c;
window.logInteraction = () => {};
window.escapeHtml = (text) => text ? text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;") : '';
""")

# Evaluate script.js logic (only the needed functions)
await page.evaluate("""
window.prepareSearchIndex = function(data) {
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
const now = new Date().getTime();
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;

for (let i = 0; i < data.length; i++) {
const pdf = data[i];

pdf._searchStr = (
(pdf.title || '') + ' ' +
(pdf.description || '') + ' ' +
(pdf.category || '') + ' ' +
(pdf.author || '')
).toLowerCase();

const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj)) {
pdf._formattedDate = dateFormatter.format(uploadDateObj);
pdf._isNew = (now - uploadDateObj.getTime()) < SEVEN_DAYS;
} else {
pdf._formattedDate = 'Unknown Date';
pdf._isNew = false;
}
}
};

window.createPDFCard = function(pdf, favoritesList, index = 0, highlightRegex = null) {
const favorites = favoritesList || window.getFavorites();
const isFav = favorites.includes(pdf.id);
const heartIconClass = isFav ? 'fas' : 'far';
const btnActiveClass = isFav ? 'active' : '';

let isNew = pdf._isNew;
let formattedDate = pdf._formattedDate;

if (isNew === undefined || !formattedDate) {
const uploadDateObj = new Date(pdf.uploadDate);
if (!isNaN(uploadDateObj)) {
const timeDiff = new Date() - uploadDateObj;
isNew = timeDiff < (7 * 24 * 60 * 60 * 1000);
formattedDate = uploadDateObj.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});
} else {
isNew = false;
formattedDate = 'Unknown Date';
}
}

const newBadgeHTML = isNew
? `<span style="background:var(--error-color); color:white; font-size:0.6rem; padding:2px 6px; border-radius:4px; margin-left:8px; vertical-align:middle;">NEW</span>`
: '';

const highlightText = (text) => {
const safeText = window.escapeHtml(text);
if (!highlightRegex) return safeText;
return safeText.replace(highlightRegex, '<span class="highlight">$1</span>');
};

return `
<div class="pdf-card" data-category="${pdf.category}">
<div class="pdf-header">
<div class="pdf-info"><h3>${highlightText(pdf.title)} ${newBadgeHTML}</h3></div>
</div>
<div class="pdf-meta">
<div class="pdf-date">${formattedDate}</div>
</div>
</div>
`;
};

window.renderPDFs = function() {
const searchTerm = searchInput.value.toLowerCase();
const favorites = getFavorites();
const rawSearchTerm = searchInput.value.trim();
let highlightRegex = null;

const filteredPdfs = pdfDatabase.filter(pdf => {
if (pdf.semester !== currentSemester) return false;
if (pdf.class !== currentClass) return false;

if (currentCategory === 'favorites') {
if (!favorites.includes(pdf.id)) return false;
} else if (currentCategory !== 'all' && pdf.category !== currentCategory) {
return false;
}

if (searchTerm) {
if (!pdf._searchStr) return false;
if (!pdf._searchStr.includes(searchTerm)) return false;
}

return true;
});

updatePDFCount(filteredPdfs.length);

if (filteredPdfs.length === 0) {
pdfGrid.style.display = 'none';
emptyState.style.display = 'block';
return;
}

pdfGrid.style.display = 'grid';
emptyState.style.display = 'none';

let gridHTML = "";
filteredPdfs.forEach((pdf, index) => {
gridHTML += createPDFCard(pdf, favorites, index, highlightRegex);
});

pdfGrid.innerHTML = gridHTML;
};
""")

# Test 1: Basic Rendering
await page.evaluate("""
window.pdfDatabase = [
{ id: '1', title: 'Organic Chemistry Notes', class: 'MSc Chemistry', semester: 1, category: 'Organic', uploadDate: '2023-01-01', description: 'Notes on organic chemistry', author: 'Dr. Smith' },
{ id: '2', title: 'Inorganic Chemistry Notes', class: 'MSc Chemistry', semester: 1, category: 'Inorganic', uploadDate: '2023-01-02', description: 'Notes on inorganic chemistry', author: 'Dr. Jones' }
];
window.prepareSearchIndex(window.pdfDatabase);
window.renderPDFs();
""")

pdf_count = await page.evaluate("document.querySelectorAll('.pdf-card').length")
print(f"Initial PDF count: {pdf_count}")

# Test 2: Search Rendering
await page.fill('#searchInput', 'organic')
await page.evaluate("window.renderPDFs()")

pdf_count_search = await page.evaluate("document.querySelectorAll('.pdf-card').length")
print(f"PDF count after search 'organic': {pdf_count_search}")

indexed_item = await page.evaluate("pdfDatabase[0]._searchStr")
print(f"Indexed item search string: {indexed_item}")

is_new_calculated = await page.evaluate("pdfDatabase[0]._isNew !== undefined")
print(f"Is new calculated: {is_new_calculated}")

formatted_date_calculated = await page.evaluate("pdfDatabase[0]._formattedDate !== undefined")
print(f"Formatted date calculated: {formatted_date_calculated}")

await browser.close()

asyncio.run(main())