Add profile-aware feedback with vibe context
Feedback buttons now target the listener's profile instead of the dashboard filter profile. Adds a persistent vibe context input that replaces the hardcoded "general" vibe. Shows listener profile badge and track ID on each listen item. Adds manual feedback form for submitting feedback on any track by ID. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,7 @@ async def status_page(
|
|||||||
"artist": track.artist,
|
"artist": track.artist,
|
||||||
"speaker": event.speaker_name or "Unknown",
|
"speaker": event.speaker_name or "Unknown",
|
||||||
"listened_at": event.listened_at,
|
"listened_at": event.listened_at,
|
||||||
|
"profile_id": event.profile_id,
|
||||||
}
|
}
|
||||||
for event, track in recent_rows
|
for event, track in recent_rows
|
||||||
]
|
]
|
||||||
@@ -111,6 +112,13 @@ async def status_page(
|
|||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
# Map profile_id to name for recent listens
|
||||||
|
profile_name_by_id: dict[int, str] = {
|
||||||
|
profile.id: profile.name for profile, *_ in profile_rows
|
||||||
|
}
|
||||||
|
for listen in recent_listens:
|
||||||
|
listen["profile_name"] = profile_name_by_id.get(listen.pop("profile_id"), "default")
|
||||||
|
|
||||||
# Speaker mappings keyed by profile_id
|
# Speaker mappings keyed by profile_id
|
||||||
mapping_rows = (await session.execute(select(SpeakerProfileMapping))).scalars().all()
|
mapping_rows = (await session.execute(select(SpeakerProfileMapping))).scalars().all()
|
||||||
speakers_by_profile: dict[int, list[str]] = {}
|
speakers_by_profile: dict[int, list[str]] = {}
|
||||||
|
|||||||
@@ -283,6 +283,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Vibe Context -->
|
||||||
|
<div class="section" style="padding: 0.75rem 1.25rem;">
|
||||||
|
<div class="form-row" style="margin-bottom: 0;">
|
||||||
|
<span class="form-label" style="min-width: auto; color: #8b949e;">Vibe context</span>
|
||||||
|
<input id="vibe-context" class="input-field" placeholder="e.g. chill morning vibes" />
|
||||||
|
</div>
|
||||||
|
<div style="color: #484f58; font-size: 0.7rem; margin-top: 0.25rem; margin-left: calc(80px + 0.5rem);">
|
||||||
|
Feedback is contextual — describes what mood this feedback applies to
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Recent Listens -->
|
<!-- Recent Listens -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Recent Listens</h2>
|
<h2>Recent Listens</h2>
|
||||||
@@ -292,11 +303,11 @@
|
|||||||
<div class="listen-content">
|
<div class="listen-content">
|
||||||
<span class="listen-title">{{ listen.title }}</span>
|
<span class="listen-title">{{ listen.title }}</span>
|
||||||
<span class="listen-artist"> — {{ listen.artist }}</span>
|
<span class="listen-artist"> — {{ listen.artist }}</span>
|
||||||
<div class="listen-meta">{{ listen.speaker }} · {{ listen.listened_at | timeago }}</div>
|
<div class="listen-meta">{{ listen.speaker }} · <span class="profile-badge">{{ listen.profile_name }}</span> · #{{ listen.track_id }} · {{ listen.listened_at | timeago }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="feedback-btns">
|
<div class="feedback-btns">
|
||||||
<button class="fb-btn fb-up" onclick="submitFeedback(this, {{ listen.track_id }}, 'up')" title="Thumbs up">▲</button>
|
<button class="fb-btn fb-up" onclick="submitFeedback(this, {{ listen.track_id }}, 'up', '{{ listen.profile_name }}')" title="Thumbs up">▲</button>
|
||||||
<button class="fb-btn fb-down" onclick="submitFeedback(this, {{ listen.track_id }}, 'down')" title="Thumbs down">▼</button>
|
<button class="fb-btn fb-down" onclick="submitFeedback(this, {{ listen.track_id }}, 'down', '{{ listen.profile_name }}')" title="Thumbs down">▼</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -410,8 +421,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="feedback-btns">
|
<div class="feedback-btns">
|
||||||
<button class="fb-btn fb-up" onclick="submitFeedback(this, {{ track.track_id }}, 'up')" title="Thumbs up">▲</button>
|
<button class="fb-btn fb-up" onclick="submitFeedback(this, {{ track.track_id }}, 'up', CURRENT_PROFILE)" title="Thumbs up">▲</button>
|
||||||
<button class="fb-btn fb-down" onclick="submitFeedback(this, {{ track.track_id }}, 'down')" title="Thumbs down">▼</button>
|
<button class="fb-btn fb-down" onclick="submitFeedback(this, {{ track.track_id }}, 'down', CURRENT_PROFILE)" title="Thumbs down">▼</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -424,6 +435,20 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Actions</h2>
|
<h2>Actions</h2>
|
||||||
|
<div class="form-row">
|
||||||
|
<span class="form-label">Feedback</span>
|
||||||
|
<input id="manual-fb-track" type="number" class="input-field input-sm" placeholder="#" style="width: 70px;" />
|
||||||
|
<select id="manual-fb-signal" class="input-field" style="width: auto;">
|
||||||
|
<option value="up">▲ Up</option>
|
||||||
|
<option value="down">▼ Down</option>
|
||||||
|
</select>
|
||||||
|
<select id="manual-fb-profile" class="input-field" style="width: auto;">
|
||||||
|
{% for pname in all_profile_names %}
|
||||||
|
<option value="{{ pname }}" {{ 'selected' if (selected_profile == pname) or (not selected_profile and pname == 'default') else '' }}>{{ pname }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button class="action-btn" onclick="submitManualFeedback()">Send</button>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<span class="form-label">Discover</span>
|
<span class="form-label">Discover</span>
|
||||||
<input id="discover-limit" type="number" class="input-field input-sm" value="50" min="1" max="500" style="width: 70px;" />
|
<input id="discover-limit" type="number" class="input-field input-sm" value="50" min="1" max="500" style="width: 70px;" />
|
||||||
@@ -671,20 +696,51 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Feedback ---
|
// --- Feedback ---
|
||||||
async function submitFeedback(btn, trackId, signal) {
|
function getVibeContext() {
|
||||||
|
return document.getElementById('vibe-context').value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitFeedback(btn, trackId, signal, profileName) {
|
||||||
|
const vibe = getVibeContext();
|
||||||
|
if (!vibe) {
|
||||||
|
showToast('Set a vibe context first', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const profile = profileName || CURRENT_PROFILE;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const res = await apiCall('POST', '/api/profiles/' + CURRENT_PROFILE + '/feedback', {
|
const res = await apiCall('POST', '/api/profiles/' + profile + '/feedback', {
|
||||||
track_id: trackId, signal: signal, vibe: 'general'
|
track_id: trackId, signal: signal, vibe: vibe
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
btn.classList.add('sent');
|
btn.classList.add('sent');
|
||||||
showToast('Feedback recorded: ' + signal, 'success');
|
showToast('Feedback: ' + signal + ' on #' + trackId + ' → ' + profile + ' (' + vibe + ')', 'success');
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
showToast('Feedback failed: ' + res.error, 'error');
|
showToast('Feedback failed: ' + res.error, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitManualFeedback() {
|
||||||
|
const trackId = parseInt(document.getElementById('manual-fb-track').value);
|
||||||
|
const signal = document.getElementById('manual-fb-signal').value;
|
||||||
|
const profile = document.getElementById('manual-fb-profile').value;
|
||||||
|
const vibe = getVibeContext();
|
||||||
|
if (!trackId) { showToast('Enter a track ID', 'error'); return; }
|
||||||
|
if (!vibe) { showToast('Set a vibe context first', 'error'); return; }
|
||||||
|
const btn = event.target;
|
||||||
|
setLoading(btn, true);
|
||||||
|
const res = await apiCall('POST', '/api/profiles/' + profile + '/feedback', {
|
||||||
|
track_id: trackId, signal: signal, vibe: vibe
|
||||||
|
});
|
||||||
|
setLoading(btn, false);
|
||||||
|
if (res.ok) {
|
||||||
|
showToast('Feedback: ' + signal + ' on #' + trackId + ' → ' + profile + ' (' + vibe + ')', 'success');
|
||||||
|
document.getElementById('manual-fb-track').value = '';
|
||||||
|
} else {
|
||||||
|
showToast('Feedback failed: ' + res.error, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function retractFeedback(btn, eventId) {
|
async function retractFeedback(btn, eventId) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const res = await apiCall('DELETE', '/api/feedback/' + eventId);
|
const res = await apiCall('DELETE', '/api/feedback/' + eventId);
|
||||||
|
|||||||
Reference in New Issue
Block a user