Skip to content

Commit f361427

Browse files
committed
feat: add AI blurb fetching and display on book cards
1 parent 979df38 commit f361427

1 file changed

Lines changed: 38 additions & 30 deletions

File tree

‎frontend/js/app.js‎

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -503,10 +503,10 @@ class BookRenderer {
503503
const title = volumeInfo.title || "Untitled";
504504
const authors = volumeInfo.authors ? volumeInfo.authors.join(", ") : "Unknown Author";
505505
const thumb = volumeInfo.imageLinks ? volumeInfo.imageLinks.thumbnail : 'https://via.placeholder.com/128x196?text=No+Cover';
506-
const description = volumeInfo.description ? volumeInfo.description.substring(0, 100) + "..." : "A mysterious tome waiting to be opened.";
506+
const originalDescription = volumeInfo.description ? volumeInfo.description.substring(0, 100) + "..." : "A mysterious tome waiting to be opened.";
507507
const categories = volumeInfo.categories || [];
508508

509-
const vibe = this.generateVibe(description, categories);
509+
const vibe = this.generateVibe(originalDescription, categories);
510510
const spineColors = ['#5D4037', '#4E342E', '#3E2723', '#2C2420', '#8D6E63'];
511511
const randomSpine = spineColors[Math.floor(Math.random() * spineColors.length)];
512512

@@ -517,24 +517,6 @@ class BookRenderer {
517517
const flipSound = new Audio('../assets/sounds/page-flip.mp3');
518518
flipSound.volume = 0.5;
519519

520-
/**
521-
* ============================================================================
522-
* SECURE HTML ESCAPING HELPER & XSS PREVENTION
523-
* ============================================================================
524-
* Problem:
525-
* Previously, book title and author data were injected directly into the
526-
* scene.innerHTML without sanitization. If data from the Google Books API
527-
* contained special HTML characters (e.g., <script> tags, <, >), this could
528-
* lead to rendering bugs or Cross-Site Scripting (XSS) vulnerabilities.
529-
*
530-
* Fix:
531-
* We introduce this `escapeHTML` helper function. It replaces sensitive
532-
* HTML characters with their harmless entity equivalents before injection.
533-
* This strictly forces the browser to treat the dynamic content as text
534-
* rather than executable code or structural markup. This is a crucial
535-
* security measure when constructing HTML strings manually.
536-
* ============================================================================
537-
*/
538520
const escapeHTML = (str) => {
539521
if (!str) return "";
540522
return String(str)
@@ -547,17 +529,13 @@ class BookRenderer {
547529

548530
const safeTitle = escapeHTML(title);
549531
const safeAuthors = escapeHTML(authors);
550-
const safeDescription = escapeHTML(description);
532+
const safeOriginalDescription = escapeHTML(originalDescription);
551533
const safeVibe = escapeHTML(vibe);
552534
const safeThumb = escapeHTML(thumb.replace('http:', 'https:'));
553535

554536
scene.innerHTML = `
555537
<div class="book" data-id="${escapeHTML(id)}">
556538
<div class="book__face book__face--front">
557-
<!--
558-
Using the sanitized title for the 'alt' attribute and
559-
sanitized URL for 'src' ensures no attribute escape attacks.
560-
-->
561539
<img src="${safeThumb}" alt="${safeTitle}">
562540
</div>
563541
<div class="book__face book__face--spine" style="background: ${randomSpine}"></div>
@@ -566,15 +544,14 @@ class BookRenderer {
566544
<div class="book__face book__face--bottom"></div>
567545
<div class="book__face book__face--back">
568546
<div style="overflow-y: auto; height: 100%; padding-right: 5px; scrollbar-width: thin;">
569-
<!-- Safe data injection using escaped values -->
570547
<div style="font-weight: bold; font-size: 0.9rem; margin-bottom: 0.5rem; color: var(--text-main);">${safeTitle}</div>
571548
<div class="handwritten-note" style="margin-bottom: 0.8rem; font-style: italic; color: var(--wood-dark);">${safeVibe}</div>
572549
${bookData.moods && bookData.moods.length > 0 ? `
573550
<div class="book-mood-tags" style="margin-bottom: 0.8rem; display: flex; flex-wrap: wrap; gap: 4px;">
574551
${bookData.moods.map(m => `<span style="font-size: 0.6rem; background: rgba(0,0,0,0.1); padding: 2px 6px; border-radius: 10px;"><i class="fa-solid ${this.getMoodIcon(m)}"></i> ${m}</span>`).join('')}
575552
</div>
576553
` : ''}
577-
<div class="book-blurb" style="font-size: 0.8rem; line-height: 1.4; color: var(--text-muted); text-align: justify;">${safeDescription}</div>
554+
<div class="book-blurb" data-book-id="${escapeHTML(id)}" style="font-size: 0.8rem; line-height: 1.4; color: var(--text-muted); text-align: justify; min-height: 60px;">${safeOriginalDescription}</div>
578555
</div>
579556
${shelf === 'current' ? `
580557
<div class="reading-progress">
@@ -590,11 +567,24 @@ class BookRenderer {
590567
</div>
591568
</div>
592569
<div class="glass-overlay">
593-
<!-- Safe author and title data injection in the overlay -->
594570
<strong>${safeTitle}</strong><br><small>${safeAuthors}</small>
595571
</div>
596572
`;
597573

574+
// Fetch AI-generated blurb asynchronously
575+
const blurbElement = scene.querySelector('.book-blurb');
576+
if (blurbElement) {
577+
this.fetchAIBlurb(id, title, authors, volumeInfo.description || "", categories)
578+
.then(aiBlurb => {
579+
if (aiBlurb && blurbElement) {
580+
blurbElement.textContent = aiBlurb;
581+
}
582+
})
583+
.catch(err => {
584+
// Silently keep fallback description
585+
});
586+
}
587+
598588
// Interaction: Progress Slider
599589
const slider = scene.querySelector('.progress-slider');
600590
if (slider) {
@@ -696,10 +686,28 @@ class BookRenderer {
696686
});
697687
if (res.ok) {
698688
const data = await res.json();
699-
return data.vibe;
689+
return data.data?.blurb || null;
690+
}
691+
} catch (e) {
692+
// Silently fail to use fallback
693+
}
694+
return null;
695+
}
696+
697+
async fetchAIBlurb(bookId, title, author, description, categories = []) {
698+
try {
699+
const res = await fetch(`${MOOD_API_BASE}/generate-note`, {
700+
method: 'POST',
701+
headers: { 'Content-Type': 'application/json' },
702+
credentials: 'include',
703+
body: JSON.stringify({ title, author, description })
704+
});
705+
if (res.ok) {
706+
const data = await res.json();
707+
return data.data?.blurb || null;
700708
}
701709
} catch (e) {
702-
// Silently fail to mock vibe
710+
// Silently fail to use fallback
703711
}
704712
return null;
705713
}

0 commit comments

Comments
 (0)