TasteEngine / templates /index.html
Abdallah4z's picture
Add user creation API and enhance user preference handling in the frontend
28e5413
Raw History Blame Contribute Delete
10.2 kB
{% extends "base.html" %}
{% block title %}TasteEngine — Home{% endblock %}
{% block content %}
{% from "macros.html" import glass, tag %}
{% call glass("Welcome to TasteEngine") %}
<p class="text-muted" style="font-size: 0.9rem;">
An intelligent multi-approach recommender system. Compare Collaborative Filtering,
Content-Based, and Knowledge-Based recommendations side by side.
</p>
{% endcall %}
<div x-data="homeApp()" class="row">
{% call glass("Select a User") %}
<div class="form-group">
<label class="form-label" for="userSelect">User</label>
<select id="userSelect" x-model="selectedUserId" @change="onUserChange">
<option value="">-- Choose a user --</option>
{% for u in users %}
<option value="{{ u.id }}">{{ u.name }} (Age: {{ u.age }})</option>
{% endfor %}
</select>
</div>
<div class="profile-card mt-2" x-show="selectedUser" x-cloak x-transition>
<div class="profile-avatar" x-text="selectedUser?.name?.charAt(0) || '?'"></div>
<div class="profile-info">
<div class="profile-name" x-text="selectedUser?.name"></div>
<div class="profile-detail" x-text="'Age: ' + selectedUser?.age"></div>
<div class="profile-detail" x-text="'Budget: $' + selectedUser?.budget_min?.toFixed(2) + ' - $' + selectedUser?.budget_max?.toFixed(2)"></div>
<div class="profile-tags mt-1">
<template x-for="cat in (selectedUser?.categories || [])" :key="cat">
<span class="tag" x-text="cat"></span>
</template>
<template x-for="b in (selectedUser?.brands || [])" :key="b">
<span class="tag tag-brand" x-text="b"></span>
</template>
</div>
</div>
</div>
<div x-show="selectedUser" x-cloak x-transition class="mt-2">
<button class="btn btn-secondary btn-sm" @click="showEditor = !showEditor">
<span x-text="showEditor ? 'Hide Preferences' : 'Edit Preferences'"></span>
</button>
</div>
<div x-show="showEditor && selectedUser" x-cloak x-transition class="mt-2">
<div class="glass" style="margin-bottom:0;padding:1rem;">
<h3 style="font-size:0.9rem;color:var(--text-primary);margin-bottom:0.75rem;">Budget Preferences</h3>
<div class="preference-slider">
<label>Min Budget</label>
<input type="range" :min="0" :max="selectedUser?.budget_max || 100" x-model.number="editBudgetMin">
<span class="slider-value" x-text="'$' + editBudgetMin"></span>
</div>
<div class="preference-slider">
<label>Max Budget</label>
<input type="range" :min="editBudgetMin" :max="500" x-model.number="editBudgetMax">
<span class="slider-value" x-text="'$' + editBudgetMax"></span>
</div>
<button class="btn btn-primary btn-sm mt-2" @click="savePreferences()" :disabled="savingPrefs">
<span x-show="!savingPrefs">Save Preferences</span>
<span x-show="savingPrefs">Saving...</span>
</button>
</div>
</div>
<div class="mt-2" x-cloak>
<button class="btn btn-secondary btn-sm" @click="showCreator = !showCreator">
<span x-text="showCreator ? 'Cancel' : '+ Create New User'"></span>
</button>
</div>
<div x-show="showCreator" x-cloak x-transition class="mt-2">
<div class="glass" style="margin-bottom:0;padding:1rem;">
<h3 style="font-size:0.9rem;color:var(--text-primary);margin-bottom:0.75rem;">New User</h3>
<div class="form-row">
<div class="form-group">
<label class="form-label">Name</label>
<input type="text" x-model="newName" placeholder="e.g. John">
</div>
<div class="form-group">
<label class="form-label">Age</label>
<input type="number" x-model.number="newAge" min="1" max="120" value="25">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Min Budget</label>
<input type="number" x-model.number="newBudgetMin" min="0" value="0">
</div>
<div class="form-group">
<label class="form-label">Max Budget</label>
<input type="number" x-model.number="newBudgetMax" min="0" value="500">
</div>
</div>
<div class="form-group">
<label class="form-label">Preferred Categories</label>
<select multiple x-model="newCategories" style="min-height:80px;">
{% for cat in categories %}
<option value="{{ cat }}">{{ cat }}</option>
{% endfor %}
</select>
<span class="form-hint">Ctrl+click to select multiple</span>
</div>
<button class="btn btn-primary btn-sm mt-2" @click="createUser()" :disabled="creatingUser">
<span x-show="!creatingUser">Create User</span>
<span x-show="creatingUser">Creating...</span>
</button>
</div>
</div>
{% endcall %}
{% call glass("Quick Actions") %}
<p class="text-muted" style="font-size: 0.85rem; margin-bottom: 1rem;">
Get started by selecting a user above, then:
</p>
<a href="/recommend" class="btn btn-primary" style="display: inline-flex; text-decoration: none; margin-bottom: 0.5rem;">Get Recommendations</a>
<br>
<a href="/evaluate" class="btn btn-secondary" style="display: inline-flex; text-decoration: none;">View Evaluation</a>
{% endcall %}
</div>
{% call glass("Available Approaches") %}
<div class="row" style="grid-template-columns: 1fr 1fr 1fr;">
<div class="text-center" style="padding: 1rem;">
<div style="font-size: 2.5rem; margin-bottom: 0.5rem;">🤝</div>
<h3 style="color: var(--text-primary); font-size: 1rem; margin-bottom: 0.3rem;">Collaborative Filtering</h3>
<p class="text-dim" style="font-size: 0.8rem;">5 methods: User, Item, SVD, KNN, Slope One</p>
</div>
<div class="text-center" style="padding: 1rem;">
<div style="font-size: 2.5rem; margin-bottom: 0.5rem;">🏷️</div>
<h3 style="color: var(--text-primary); font-size: 1rem; margin-bottom: 0.3rem;">Content-Based</h3>
<p class="text-dim" style="font-size: 0.8rem;">TF-IDF + Cosine Similarity, Feature Matching</p>
</div>
<div class="text-center" style="padding: 1rem;">
<div style="font-size: 2.5rem; margin-bottom: 0.5rem;">⚙️</div>
<h3 style="color: var(--text-primary); font-size: 1rem; margin-bottom: 0.3rem;">Knowledge-Based</h3>
<p class="text-dim" style="font-size: 0.8rem;">Constraint, Rule, Utility-based filtering</p>
</div>
</div>
{% endcall %}
{% endblock %}
{% block scripts %}
<script>
const userData = {{ users | tojson | safe }};
document.addEventListener('alpine:init', () => {
Alpine.data('homeApp', () => ({
selectedUserId: localStorage.getItem('selectedUser') || null,
selectedUser: null,
showEditor: false,
editBudgetMin: 0,
editBudgetMax: 100,
savingPrefs: false,
showCreator: false,
newName: '',
newAge: 25,
newBudgetMin: 0,
newBudgetMax: 500,
newCategories: [],
creatingUser: false,
init() {
if (this.selectedUserId) {
this.selectedUserId = parseInt(this.selectedUserId);
this.onUserChange();
}
},
onUserChange() {
const uid = this.selectedUserId ? parseInt(this.selectedUserId) : null;
if (!uid) {
this.selectedUser = null;
return;
}
this.selectedUser = userData.find(u => u.id === uid);
if (this.selectedUser) {
this.editBudgetMin = this.selectedUser.budget_min;
this.editBudgetMax = this.selectedUser.budget_max;
}
localStorage.setItem('selectedUser', uid);
},
async savePreferences() {
if (!this.selectedUser) return;
this.savingPrefs = true;
try {
const r = await fetch(`/api/user/${this.selectedUser.id}/preferences`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
budget_min: this.editBudgetMin,
budget_max: this.editBudgetMax
})
});
const data = await r.json();
if (data.success) {
this.selectedUser.budget_min = this.editBudgetMin;
this.selectedUser.budget_max = this.editBudgetMax;
toast('Preferences saved!', 'success');
this.showEditor = false;
}
} catch (err) {
toast('Error saving preferences', 'error');
} finally {
this.savingPrefs = false;
}
},
async createUser() {
if (!this.newName.trim()) { toast('Please enter a name', 'warning'); return; }
this.creatingUser = true;
try {
const r = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: this.newName.trim(),
age: this.newAge,
categories: this.newCategories,
budget_min: this.newBudgetMin,
budget_max: this.newBudgetMax,
})
});
const data = await r.json();
if (data.success) {
toast(`User "${data.name}" created!`, 'success');
this.showCreator = false;
this.newName = '';
this.newAge = 25;
this.newCategories = [];
const newUser = { id: data.user_id, name: data.name, age: this.newAge,
categories: this.newCategories, brands: [],
budget_min: this.newBudgetMin, budget_max: this.newBudgetMax };
userData.push(newUser);
this.selectedUserId = data.user_id;
this.selectedUser = newUser;
this.onUserChange();
}
} catch (err) {
toast('Error creating user: ' + err.message, 'error');
} finally {
this.creatingUser = false;
}
}
}));
});
</script>
{% endblock %}