Skip to content

Commit 8793b89

Browse files
committed
feat: Add GoodReads sentiment analysis for book mood detection
1 parent 503197d commit 8793b89

21 files changed

Lines changed: 2198 additions & 26 deletions

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
GITHUB_ISSUES.md
2+
3+
# Mood analysis cache files
4+
mood_analysis/mood_cache.json
5+
*.cache

‎.vscode/settings.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}
2.94 KB
Binary file not shown.
6.15 KB
Binary file not shown.
9.5 KB
Binary file not shown.

‎ai_service.py‎

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,73 @@
1-
# Placeholder for AI service logic.
2-
# implement 'generate_book_note' and 'get_ai_recommendations'. All recommendations MUST be AI-based.
1+
# AI service logic with GoodReads sentiment analysis integration
2+
# Implements 'generate_book_note' and 'get_ai_recommendations'. All recommendations MUST be AI-based.
33

4-
def generate_book_note(description):
4+
try:
5+
from mood_analysis.ai_service_enhanced import AIBookService, get_book_mood_tags, generate_enhanced_book_note
6+
MOOD_ANALYSIS_AVAILABLE = True
7+
except ImportError:
8+
MOOD_ANALYSIS_AVAILABLE = False
9+
print("Mood analysis not available - using fallback AI service")
10+
11+
def generate_book_note(description, title="", author=""):
512
"""
613
Analyzes book description and returns a 'vibe'.
7-
In a real scenario, this would call an LLM (like Gemini or OpenAI).
14+
Enhanced with GoodReads mood analysis when available.
815
"""
9-
# Logic: For now, we return a string based on description length
10-
if len(description) > 100:
11-
return "A deep, complex narrative with lingering mystery."
12-
return "A quick, refreshing read for a busy afternoon."
16+
if MOOD_ANALYSIS_AVAILABLE and title and author:
17+
try:
18+
return generate_enhanced_book_note(description, title, author)
19+
except Exception as e:
20+
print(f"Mood analysis failed, using fallback: {e}")
21+
22+
# Fallback to description-based analysis
23+
if len(description) > 200:
24+
return "A deep, complex narrative that readers find emotionally resonant."
25+
elif len(description) > 100:
26+
return "A compelling story with layers waiting to be discovered."
27+
elif "mystery" in description.lower():
28+
return "A mysterious tale that will keep you guessing."
29+
elif "romance" in description.lower():
30+
return "A heartwarming story perfect for cozy reading."
31+
else:
32+
return "A delightful read for any quiet moment."
1333

1434
def get_ai_recommendations(query):
15-
"""Mock AI logic to filter/rank books."""
16-
return f"AI-optimized results for: {query}"
35+
"""Enhanced AI logic to filter/rank books based on mood."""
36+
37+
# Mood-based query mapping
38+
mood_queries = {
39+
'cozy': 'comfort reads warm atmosphere',
40+
'dark': 'psychological thriller mystery',
41+
'romantic': 'romance love story',
42+
'mysterious': 'mystery suspense thriller',
43+
'uplifting': 'inspiring hopeful positive',
44+
'melancholy': 'literary fiction emotional',
45+
'adventurous': 'adventure fantasy epic'
46+
}
47+
48+
# Check if query matches a mood
49+
query_lower = query.lower()
50+
for mood, book_query in mood_queries.items():
51+
if mood in query_lower:
52+
return f"AI-optimized {mood} results: {book_query}"
53+
54+
return f"AI-optimized results for: {query}"
55+
56+
def get_book_mood_tags_safe(title: str, author: str = "") -> list:
57+
"""
58+
Safe wrapper for getting book mood tags.
59+
60+
Args:
61+
title: Book title
62+
author: Author name
63+
64+
Returns:
65+
List of mood tags or empty list if not available
66+
"""
67+
if MOOD_ANALYSIS_AVAILABLE:
68+
try:
69+
return get_book_mood_tags(title, author)
70+
except Exception as e:
71+
print(f"Error getting mood tags: {e}")
72+
73+
return []

‎app.js‎

Lines changed: 141 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
/**
2-
* BiblioDrift Core Logic
3-
* Handles 3D rendering, API fetching, and LocalStorage management.
2+
* BiblioDrift Core Logic with GoodReads Mood Analysis
3+
* Handles 3D rendering, API fetching, mood analysis, and LocalStorage management.
44
*/
55

66
const API_BASE = 'https://www.googleapis.com/books/v1/volumes';
7+
const MOOD_API_BASE = 'http://localhost:5000/api/v1';
78

89
class BookRenderer {
910
constructor() {
1011
this.libraryManager = new LibraryManager();
12+
this.moodAnalyzer = new MoodAnalyzer();
1113
}
1214

13-
createBookElement(bookData) {
15+
async createBookElement(bookData) {
1416
const { id, volumeInfo } = bookData;
1517
const title = volumeInfo.title || "Untitled";
1618
const authors = volumeInfo.authors ? volumeInfo.authors.join(", ") : "Unknown Author";
1719
const thumb = volumeInfo.imageLinks ? volumeInfo.imageLinks.thumbnail : 'https://via.placeholder.com/128x196?text=No+Cover';
1820
const description = volumeInfo.description ? volumeInfo.description.substring(0, 100) + "..." : "A mysterious tome waiting to be opened.";
1921

22+
// Try to get mood analysis for this book
23+
let vibe = this.generateVibe(description);
24+
let moodTags = [];
25+
26+
try {
27+
const moodData = await this.moodAnalyzer.getBookMood(title, authors);
28+
if (moodData && moodData.success) {
29+
vibe = moodData.mood_analysis.bibliodrift_vibe || vibe;
30+
moodTags = moodData.mood_analysis.primary_moods || [];
31+
}
32+
} catch (error) {
33+
console.log('Mood analysis not available, using fallback vibe');
34+
}
35+
2036
// Randomize spine color slightly for variety
2137
const spineColors = ['#5D4037', '#4E342E', '#3E2723', '#2C2420', '#8D6E63'];
2238
const randomSpine = spineColors[Math.floor(Math.random() * spineColors.length)];
@@ -25,33 +41,51 @@ class BookRenderer {
2541
const scene = document.createElement('div');
2642
scene.className = 'book-scene';
2743

44+
// Generate mood tags HTML
45+
const moodTagsHTML = moodTags.length > 0
46+
? `<div class="mood-tags">${moodTags.slice(0, 2).map(tag => `<span class="mood-tag mood-${tag.mood}">${tag.mood}</span>`).join('')}</div>`
47+
: '';
48+
2849
// Structure
2950
scene.innerHTML = `
3051
<div class="book" data-id="${id}">
3152
<div class="book__face book__face--front">
3253
<img src="${thumb.replace('http:', 'https:')}" alt="${title}">
54+
${moodTagsHTML}
3355
</div>
3456
<div class="book__face book__face--spine" style="background: ${randomSpine}"></div>
3557
<div class="book__face book_face--right"></div>
3658
<div class="book__face book__face--back">
3759
<div>
3860
<div style="font-weight: bold; font-size: 0.9rem; margin-bottom: 0.5rem;">${title}</div>
3961
<div class="handwritten-note">
40-
Bookseller's Note: "${this.generateVibe(description)}"
62+
Bookseller's Note: "${vibe}"
4163
</div>
64+
${moodTags.length > 0 ? `
65+
<div class="mood-analysis">
66+
<small>Mood Analysis:</small>
67+
<div class="mood-tags-back">
68+
${moodTags.slice(0, 3).map(tag => `<span class="mood-tag-small">${tag.mood}</span>`).join('')}
69+
</div>
70+
</div>` : ''}
4271
</div>
4372
<div class="book-actions">
4473
<button class="btn-icon add-btn" title="Add to Library"><i class="fa-regular fa-heart"></i></button>
74+
<button class="btn-icon mood-btn" title="Analyze Mood" onclick="event.stopPropagation(); this.closest('.book-scene').querySelector('.book-renderer').showMoodAnalysis('${title}', '${authors}')"><i class="fa-solid fa-brain"></i></button>
4575
<button class="btn-icon" title="Flip Back" onclick="event.stopPropagation(); this.closest('.book').classList.remove('flipped')"><i class="fa-solid fa-rotate-left"></i></button>
4676
</div>
4777
</div>
4878
</div>
4979
<div class="glass-overlay">
5080
<strong>${title}</strong><br>
5181
<small>${authors}</small>
82+
${moodTags.length > 0 ? `<div class="glass-mood-tags">${moodTags.slice(0, 2).map(tag => `<span class="glass-mood-tag">${tag.mood}</span>`).join('')}</div>` : ''}
5283
</div>
5384
`;
5485

86+
// Store reference for mood analysis
87+
scene.querySelector('.book-scene').bookRenderer = this;
88+
5589
// Interaction: Flip
5690
const bookEl = scene.querySelector('.book');
5791
scene.addEventListener('click', (e) => {
@@ -72,8 +106,72 @@ class BookRenderer {
72106
return scene;
73107
}
74108

109+
async showMoodAnalysis(title, author) {
110+
try {
111+
const moodData = await this.moodAnalyzer.analyzeMood(title, author);
112+
if (moodData && moodData.success) {
113+
this.displayMoodModal(title, moodData.mood_analysis);
114+
} else {
115+
alert('Mood analysis not available for this book.');
116+
}
117+
} catch (error) {
118+
console.error('Error showing mood analysis:', error);
119+
alert('Error loading mood analysis.');
120+
}
121+
}
122+
123+
displayMoodModal(title, moodAnalysis) {
124+
const modal = document.createElement('div');
125+
modal.className = 'mood-modal';
126+
modal.innerHTML = `
127+
<div class="mood-modal-content">
128+
<div class="mood-modal-header">
129+
<h3>Mood Analysis: ${title}</h3>
130+
<button class="close-modal">&times;</button>
131+
</div>
132+
<div class="mood-modal-body">
133+
<div class="mood-section">
134+
<h4>Overall Sentiment</h4>
135+
<div class="sentiment-bar">
136+
<div class="sentiment-fill" style="width: ${(moodAnalysis.overall_sentiment.compound_score + 1) * 50}%"></div>
137+
</div>
138+
<p>${moodAnalysis.mood_description}</p>
139+
</div>
140+
<div class="mood-section">
141+
<h4>Primary Moods</h4>
142+
<div class="mood-tags-large">
143+
${moodAnalysis.primary_moods.map(mood =>
144+
`<span class="mood-tag-large mood-${mood.mood}">${mood.mood} (${mood.frequency})</span>`
145+
).join('')}
146+
</div>
147+
</div>
148+
<div class="mood-section">
149+
<h4>BiblioDrift Vibe</h4>
150+
<div class="vibe-quote">"${moodAnalysis.bibliodrift_vibe}"</div>
151+
</div>
152+
<div class="mood-section">
153+
<small>Based on ${moodAnalysis.total_reviews_analyzed} GoodReads reviews</small>
154+
</div>
155+
</div>
156+
</div>
157+
`;
158+
159+
document.body.appendChild(modal);
160+
161+
// Close modal functionality
162+
modal.querySelector('.close-modal').addEventListener('click', () => {
163+
document.body.removeChild(modal);
164+
});
165+
166+
modal.addEventListener('click', (e) => {
167+
if (e.target === modal) {
168+
document.body.removeChild(modal);
169+
}
170+
});
171+
}
172+
75173
generateVibe(text) {
76-
// Simple heuristic to mock "AI" vibes
174+
// Simple heuristic to mock "AI" vibes (fallback)
77175
const vibes = [
78176
"Perfect for a rainy afternoon.",
79177
"Smells like old paper and adventure.",
@@ -94,9 +192,10 @@ class BookRenderer {
94192

95193
if (data.items) {
96194
container.innerHTML = '';
97-
data.items.forEach(book => {
98-
container.appendChild(this.createBookElement(book));
99-
});
195+
for (const book of data.items) {
196+
const bookElement = await this.createBookElement(book);
197+
container.appendChild(bookElement);
198+
}
100199
}
101200
} catch (err) {
102201
console.error("Failed to fetch books", err);
@@ -105,6 +204,40 @@ class BookRenderer {
105204
}
106205
}
107206

207+
class MoodAnalyzer {
208+
async getBookMood(title, author) {
209+
try {
210+
const response = await fetch(`${MOOD_API_BASE}/mood-tags`, {
211+
method: 'POST',
212+
headers: {
213+
'Content-Type': 'application/json',
214+
},
215+
body: JSON.stringify({ title, author })
216+
});
217+
return await response.json();
218+
} catch (error) {
219+
console.error('Error fetching mood tags:', error);
220+
return null;
221+
}
222+
}
223+
224+
async analyzeMood(title, author) {
225+
try {
226+
const response = await fetch(`${MOOD_API_BASE}/analyze-mood`, {
227+
method: 'POST',
228+
headers: {
229+
'Content-Type': 'application/json',
230+
},
231+
body: JSON.stringify({ title, author })
232+
});
233+
return await response.json();
234+
} catch (error) {
235+
console.error('Error analyzing mood:', error);
236+
return null;
237+
}
238+
}
239+
}
240+
108241
class LibraryManager {
109242
constructor() {
110243
this.storageKey = 'bibliodrift_library';

0 commit comments

Comments
 (0)