Skip to content

Commit 0601012

Browse files
Merge pull request #853: Optimize backend imports and wire up Reader Identity UI
2 parents ad41e70 + 60ca62b commit 0601012

8 files changed

Lines changed: 253 additions & 80 deletions

File tree

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""
22
Handles unsupervised reader clustering.
33
"""
4-
from sklearn.feature_extraction.text import TfidfVectorizer
5-
from sklearn.cluster import KMeans
64
"""
75
Uses TF-IDF and KMeans clustering for grouping readers.
86
"""
@@ -11,29 +9,39 @@
119
class ClusterEngine:
1210

1311
def __init__(self):
14-
15-
self.vectorizer = TfidfVectorizer(
16-
stop_words="english"
17-
)
12+
self.vectorizer = None
1813

1914
def cluster_reviews(self, reviews):
20-
21-
vectors = self.vectorizer.fit_transform(
22-
reviews
23-
)
24-
25-
cluster_count = min(
26-
len(reviews),
27-
4
28-
)
29-
30-
model = KMeans(
31-
n_clusters=cluster_count,
32-
random_state=42
33-
)
34-
35-
clusters = model.fit_predict(
36-
vectors
37-
)
38-
39-
return clusters.tolist()
15+
if not reviews:
16+
return [-1]
17+
18+
try:
19+
from sklearn.cluster import KMeans
20+
if self.vectorizer is None:
21+
from sklearn.feature_extraction.text import TfidfVectorizer
22+
self.vectorizer = TfidfVectorizer(
23+
stop_words="english"
24+
)
25+
vectors = self.vectorizer.fit_transform(
26+
reviews
27+
)
28+
29+
cluster_count = min(
30+
len(reviews),
31+
4
32+
)
33+
if cluster_count == 0:
34+
return [-1]
35+
36+
model = KMeans(
37+
n_clusters=cluster_count,
38+
random_state=42
39+
)
40+
41+
clusters = model.fit_predict(
42+
vectors
43+
)
44+
45+
return clusters.tolist()
46+
except Exception:
47+
return [-1]

‎backend/reader_identity/embedding_engine.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""
22
Handles semantic similarity using sentence embeddings.
33
"""
4-
from sentence_transformers import SentenceTransformer
5-
from sklearn.metrics.pairwise import cosine_similarity
64

75
"""
86
Generates embeddings and compares semantic similarity.
@@ -12,16 +10,18 @@
1210
class EmbeddingEngine:
1311

1412
def __init__(self):
15-
16-
self.model = SentenceTransformer(
17-
"all-MiniLM-L6-v2"
18-
)
13+
self.model = None
1914

2015
def get_embedding(self, text):
21-
16+
if self.model is None:
17+
from sentence_transformers import SentenceTransformer
18+
self.model = SentenceTransformer(
19+
"all-MiniLM-L6-v2"
20+
)
2221
return self.model.encode([text])
2322

2423
def compare_texts(self, text1, text2):
24+
from sklearn.metrics.pairwise import cosine_similarity
2525

2626
embedding1 = self.get_embedding(text1)
2727

‎backend/reader_identity/routes.py‎

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,9 @@
1414
"/api/v1/reader-archetype",
1515
methods=["POST"]
1616
)
17-
1817
def reader_archetype():
19-
2018
try:
21-
2219
data = request.get_json()
23-
24-
if not data:
25-
26-
return jsonify({
27-
"success": False,
28-
"error": "Missing JSON body"
29-
}), 400
30-
31-
genres = data.get("genres", [])
32-
33-
reviews = data.get("reviews", [])
34-
35-
if not reviews:
36-
37-
return jsonify({
38-
"success": False,
39-
"error": "Reviews are required"
40-
}), 400
41-
42-
result = generator.generate_profile(
43-
genres,
44-
reviews
45-
)
46-
47-
return jsonify(result)
48-
49-
except Exception as e:
50-
51-
return jsonify({
52-
"success": False,
53-
"error": str(e)
54-
}), 500
55-
def reader_archetype():
56-
57-
try:
58-
59-
data = request.get_json()
60-
6120
if not data:
6221
return jsonify({
6322
"success": False,
@@ -83,11 +42,9 @@ def reader_archetype():
8342
genres,
8443
reviews
8544
)
86-
8745
return jsonify(result), 200
8846

8947
except Exception as e:
90-
9148
return jsonify({
9249
"success": False,
9350
"error": str(e)

‎backend/reader_identity/sentiment_engine.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
Handles sentiment analysis for reader reviews.
33
"""
44

5-
from textblob import TextBlob
6-
75
"""
86
Performs sentiment-based emotional profiling.
97
"""
108

119
class SentimentEngine:
1210

1311
def analyze_reviews(self, reviews):
12+
from textblob import TextBlob
1413

1514
combined_text = " ".join(reviews)
1615

‎frontend/js/app.js‎

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,6 +3205,204 @@ document.addEventListener('DOMContentLoaded', async () => {
32053205

32063206
loadExtendedStats();
32073207

3208+
// =====================================================================
3209+
// READER IDENTITY LOGIC
3210+
// Fetches reviews, determines archetype/mood/cluster, and renders states.
3211+
// =====================================================================
3212+
const renderErrorIdentityState = () => {
3213+
const identityContent = document.getElementById('reader-identity-content');
3214+
if (!identityContent) return;
3215+
3216+
identityContent.innerHTML = `
3217+
<div class="error-state" style="padding: 1.5rem; text-align: center; color: var(--text-muted); background: rgba(229, 57, 53, 0.05); border: 1px solid rgba(229, 57, 53, 0.2); border-radius: 10px;">
3218+
<i class="fa-solid fa-triangle-exclamation" style="font-size: 2rem; color: #e53935; margin-bottom: 1rem; display: block;"></i>
3219+
<p style="margin-bottom: 1rem;">Failed to load Reader Identity profile.</p>
3220+
<button id="retry-identity-btn" class="action-btn-secondary" style="font-size: 0.8rem; padding: 4px 10px;">Retry</button>
3221+
</div>
3222+
`;
3223+
3224+
const retryBtn = document.getElementById('retry-identity-btn');
3225+
if (retryBtn) {
3226+
retryBtn.addEventListener('click', () => {
3227+
loadReaderIdentity();
3228+
});
3229+
}
3230+
};
3231+
3232+
const fetchAndRenderArchetype = async (genres, reviewsList) => {
3233+
const identityContent = document.getElementById('reader-identity-content');
3234+
if (!identityContent) return;
3235+
3236+
const token = SafeStorage.get('bibliodrift_token');
3237+
const response = await fetch(`${MOOD_API_BASE}/reader-archetype`, {
3238+
method: 'POST',
3239+
headers: {
3240+
'Authorization': `Bearer ${token}`,
3241+
'Content-Type': 'application/json'
3242+
},
3243+
body: JSON.stringify({ genres, reviews: reviewsList })
3244+
});
3245+
3246+
if (!response.ok) {
3247+
throw new Error(`Failed to fetch archetype (Status: ${response.status})`);
3248+
}
3249+
3250+
const data = await response.json();
3251+
if (!data.success || !data.reader_profile) {
3252+
throw new Error('API returned unsuccessful profile generation');
3253+
}
3254+
3255+
const profile = data.reader_profile;
3256+
const archetype = profile.archetype || "Unknown Reader";
3257+
const mood = profile.reader_mood || "Balanced Analytical Reader";
3258+
const sentimentScore = typeof profile.sentiment_score === 'number' ? profile.sentiment_score.toFixed(2) : '0.00';
3259+
const cluster = typeof profile.reader_cluster === 'number' ? (profile.reader_cluster === -1 ? 'Unclassified' : `Group #${profile.reader_cluster}`) : 'Unclassified';
3260+
3261+
const archetypeDescriptions = {
3262+
"Deep Thinker": "Drawn to philosophy, existential questions, and reflective/psychological themes.",
3263+
"Emotional Reader": "Connects deeply with romance, relationships, emotional journeys, and human feelings.",
3264+
"Dark Reader": "Enjoys crime, violence, psychological thrillers, horror, and mystery.",
3265+
"Adventurous Reader": "Seeks sci-fi, fantasy, action-packed adventures, and exploration."
3266+
};
3267+
const desc = archetypeDescriptions[archetype] || "Based on your current reading habits and preferences.";
3268+
3269+
identityContent.innerHTML = `
3270+
<div class="reader-identity-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-top: 1rem; text-align: left;">
3271+
<div class="identity-card" style="background: rgba(255, 255, 255, 0.03); padding: 1.2rem; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.05);">
3272+
<div style="font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem; letter-spacing: 0.5px;">Reader Archetype</div>
3273+
<div style="font-size: 1.3rem; font-weight: 600; color: var(--accent-gold); display: flex; align-items: center; gap: 8px;">
3274+
<i class="fa-solid fa-brain" style="font-size: 1.3rem; margin: 0; color: var(--accent-gold);"></i>
3275+
<span id="identity-archetype">${archetype}</span>
3276+
</div>
3277+
<div style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.5rem;" id="identity-archetype-desc">
3278+
${desc}
3279+
</div>
3280+
</div>
3281+
3282+
<div class="identity-card" style="background: rgba(255, 255, 255, 0.03); padding: 1.2rem; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.05);">
3283+
<div style="font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem; letter-spacing: 0.5px;">Reader Mood</div>
3284+
<div style="font-size: 1.3rem; font-weight: 600; color: var(--accent-gold); display: flex; align-items: center; gap: 8px;">
3285+
<i class="fa-solid fa-masks-theater" style="font-size: 1.3rem; margin: 0; color: var(--accent-gold);"></i>
3286+
<span id="identity-mood">${mood}</span>
3287+
</div>
3288+
<div style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.5rem;">
3289+
Sentiment Score: <strong id="identity-sentiment">${sentimentScore}</strong>
3290+
</div>
3291+
</div>
3292+
3293+
<div class="identity-card" style="background: rgba(255, 255, 255, 0.03); padding: 1.2rem; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.05);">
3294+
<div style="font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem; letter-spacing: 0.5px;">Reader Group</div>
3295+
<div style="font-size: 1.3rem; font-weight: 600; color: var(--accent-gold); display: flex; align-items: center; gap: 8px;">
3296+
<i class="fa-solid fa-people-group" style="font-size: 1.3rem; margin: 0; color: var(--accent-gold);"></i>
3297+
<span id="identity-cluster">${cluster}</span>
3298+
</div>
3299+
<div style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.5rem;">
3300+
Based on review text analysis.
3301+
</div>
3302+
</div>
3303+
</div>
3304+
`;
3305+
};
3306+
3307+
const renderEmptyIdentityState = (genres) => {
3308+
const identityContent = document.getElementById('reader-identity-content');
3309+
if (!identityContent) return;
3310+
3311+
identityContent.innerHTML = `
3312+
<div class="empty-state" style="padding: 1rem 0; text-align: center; color: var(--text-muted);">
3313+
<i class="fa-solid fa-circle-info" style="font-size: 2rem; color: var(--accent-gold); margin-bottom: 1rem; display: block;"></i>
3314+
<p style="margin-bottom: 1.5rem;">We couldn't find any reviews in your profile yet. Add reviews to your finished books to unlock your reader identity!</p>
3315+
<div style="max-width: 450px; margin: 0 auto; background: rgba(255, 255, 255, 0.02); padding: 1.5rem; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.05); text-align: left;">
3316+
<h4 style="color: var(--text-main); margin-bottom: 0.5rem;">Or try a quick test right now:</h4>
3317+
<p style="font-size: 0.85rem; margin-bottom: 1rem;">Write a brief summary of the kinds of books you love reading (e.g. "I love deep space adventures with complex characters"):</p>
3318+
<textarea id="onboarding-review-text" placeholder="I love exploring dark mystery novels and fast-paced thrillers..." style="width: 100%; height: 80px; padding: 10px; border-radius: 8px; border: 1px solid var(--border-color); background: var(--card-bg); color: var(--text-color); margin-bottom: 1rem; font-family: inherit; font-size: 0.9rem; resize: none;"></textarea>
3319+
<button id="submit-onboarding-btn" class="action-btn-primary" style="font-size: 0.85rem; padding: 6px 15px;">Analyze Mood & Archetype</button>
3320+
</div>
3321+
</div>
3322+
`;
3323+
3324+
const submitBtn = document.getElementById('submit-onboarding-btn');
3325+
if (submitBtn) {
3326+
submitBtn.addEventListener('click', async () => {
3327+
const textInput = document.getElementById('onboarding-review-text').value.trim();
3328+
if (!textInput) return;
3329+
3330+
identityContent.innerHTML = `
3331+
<div class="loading-state" style="padding: 2rem 0; text-align: center; color: var(--text-muted);">
3332+
<i class="fa-solid fa-spinner fa-spin" style="font-size: 2rem; color: var(--accent-gold); margin-bottom: 1rem; display: block;"></i>
3333+
<p>Analyzing your custom input...</p>
3334+
</div>
3335+
`;
3336+
3337+
try {
3338+
await fetchAndRenderArchetype(genres, [textInput]);
3339+
} catch (error) {
3340+
console.error('Error analyzing custom onboarding input:', error);
3341+
renderErrorIdentityState();
3342+
}
3343+
});
3344+
}
3345+
};
3346+
3347+
const loadReaderIdentity = async () => {
3348+
const identityContent = document.getElementById('reader-identity-content');
3349+
if (!identityContent) return;
3350+
3351+
identityContent.innerHTML = `
3352+
<div class="loading-state" style="padding: 2rem 0; text-align: center; color: var(--text-muted);">
3353+
<i class="fa-solid fa-spinner fa-spin" style="font-size: 2rem; color: var(--accent-gold); margin-bottom: 1rem; display: block;"></i>
3354+
<p>Analyzing your reading profile and reviews...</p>
3355+
</div>
3356+
`;
3357+
3358+
const token = SafeStorage.get('bibliodrift_token');
3359+
if (!token) {
3360+
identityContent.innerHTML = `
3361+
<div class="error-state" style="padding: 1.5rem; text-align: center; color: var(--text-muted);">
3362+
<p>Please log in to view your Reader Identity.</p>
3363+
</div>
3364+
`;
3365+
return;
3366+
}
3367+
3368+
try {
3369+
// 1. Fetch user reviews
3370+
const reviewsResponse = await fetch(`${MOOD_API_BASE}/users/${user.id}/reviews`, {
3371+
headers: { 'Authorization': `Bearer ${token}` }
3372+
});
3373+
3374+
if (!reviewsResponse.ok) {
3375+
throw new Error(`Failed to fetch reviews (Status: ${reviewsResponse.status})`);
3376+
}
3377+
3378+
const reviewsData = await reviewsResponse.json();
3379+
const reviewsList = (reviewsData.reviews || []).map(r => r.review_text).filter(Boolean);
3380+
3381+
// Derive genres
3382+
const allBooks = [
3383+
...(libManager.library.current || []),
3384+
...(libManager.library.want || []),
3385+
...(libManager.library.finished || [])
3386+
];
3387+
const genres = Array.from(new Set(
3388+
allBooks.flatMap(book => book.volumeInfo?.categories || [])
3389+
));
3390+
3391+
// 2. Render empty/onboarding or loaded state
3392+
if (reviewsList.length === 0) {
3393+
renderEmptyIdentityState(genres);
3394+
} else {
3395+
await fetchAndRenderArchetype(genres, reviewsList);
3396+
}
3397+
} catch (error) {
3398+
console.error('Error loading reader identity:', error);
3399+
renderErrorIdentityState();
3400+
}
3401+
};
3402+
3403+
// Initialize reader identity loading
3404+
loadReaderIdentity();
3405+
32083406
// Progress Bar Calculation
32093407
const barFinished = document.getElementById('bar-finished');
32103408
const barCurrent = document.getElementById('bar-current');

0 commit comments

Comments
 (0)