Add vibe-contextual feedback system with dashboard observability
Adds feedback API endpoints that record up/down/skip signals tied to vibe context (CLAP embeddings + text). Dashboard now shows Feedback Activity (recent events with signal counts) and Vibe Influence (how the same track gets rated differently across vibes). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from haunt_fm.api.status import status as get_status_data
|
||||
from haunt_fm.db import get_session
|
||||
from haunt_fm.models.track import (
|
||||
FeedbackEvent,
|
||||
ListenEvent,
|
||||
Playlist,
|
||||
Profile,
|
||||
@@ -120,6 +121,102 @@ async def status_page(request: Request, session: AsyncSession = Depends(get_sess
|
||||
"updated_at": tp.updated_at,
|
||||
}
|
||||
|
||||
# Feedback summary stats
|
||||
feedback_total = (
|
||||
await session.execute(select(func.count(FeedbackEvent.id)))
|
||||
).scalar() or 0
|
||||
|
||||
feedback_by_signal: dict[str, int] = {}
|
||||
if feedback_total > 0:
|
||||
signal_rows = (
|
||||
await session.execute(
|
||||
select(FeedbackEvent.signal, func.count(FeedbackEvent.id))
|
||||
.group_by(FeedbackEvent.signal)
|
||||
)
|
||||
).all()
|
||||
feedback_by_signal = {signal: count for signal, count in signal_rows}
|
||||
|
||||
feedback_distinct_tracks = 0
|
||||
if feedback_total > 0:
|
||||
feedback_distinct_tracks = (
|
||||
await session.execute(
|
||||
select(func.count(func.distinct(FeedbackEvent.track_id)))
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
feedback_summary = {
|
||||
"total": feedback_total,
|
||||
"up": feedback_by_signal.get("up", 0),
|
||||
"down": feedback_by_signal.get("down", 0),
|
||||
"skip": feedback_by_signal.get("skip", 0),
|
||||
"tracks": feedback_distinct_tracks,
|
||||
}
|
||||
|
||||
# Recent feedback events (last 15)
|
||||
recent_feedback: list[dict] = []
|
||||
if feedback_total > 0:
|
||||
feedback_rows = (
|
||||
await session.execute(
|
||||
select(FeedbackEvent, Track)
|
||||
.join(Track, FeedbackEvent.track_id == Track.id)
|
||||
.order_by(FeedbackEvent.created_at.desc())
|
||||
.limit(15)
|
||||
)
|
||||
).all()
|
||||
recent_feedback = [
|
||||
{
|
||||
"signal": event.signal,
|
||||
"signal_weight": event.signal_weight,
|
||||
"title": track.title,
|
||||
"artist": track.artist,
|
||||
"vibe_text": event.vibe_text or "no vibe",
|
||||
"created_at": event.created_at,
|
||||
}
|
||||
for event, track in feedback_rows
|
||||
]
|
||||
|
||||
# Vibe influence data — top 10 tracks by feedback count
|
||||
vibe_influence: list[dict] = []
|
||||
if feedback_total > 0:
|
||||
top_track_ids_result = (
|
||||
await session.execute(
|
||||
select(FeedbackEvent.track_id, func.count(FeedbackEvent.id).label("cnt"))
|
||||
.group_by(FeedbackEvent.track_id)
|
||||
.order_by(func.count(FeedbackEvent.id).desc())
|
||||
.limit(10)
|
||||
)
|
||||
).all()
|
||||
top_track_ids = [row[0] for row in top_track_ids_result]
|
||||
|
||||
if top_track_ids:
|
||||
influence_rows = (
|
||||
await session.execute(
|
||||
select(FeedbackEvent, Track)
|
||||
.join(Track, FeedbackEvent.track_id == Track.id)
|
||||
.where(FeedbackEvent.track_id.in_(top_track_ids))
|
||||
.order_by(FeedbackEvent.created_at.desc())
|
||||
)
|
||||
).all()
|
||||
|
||||
tracks_map: dict[int, dict] = {}
|
||||
for event, track in influence_rows:
|
||||
if track.id not in tracks_map:
|
||||
tracks_map[track.id] = {
|
||||
"title": track.title,
|
||||
"artist": track.artist,
|
||||
"vibes": [],
|
||||
}
|
||||
tracks_map[track.id]["vibes"].append({
|
||||
"vibe_text": event.vibe_text or "no vibe",
|
||||
"signal": event.signal,
|
||||
"created_at": event.created_at,
|
||||
})
|
||||
|
||||
# Preserve the top-by-count ordering
|
||||
for tid in top_track_ids:
|
||||
if tid in tracks_map:
|
||||
vibe_influence.append(tracks_map[tid])
|
||||
|
||||
# Recent playlists (last 5)
|
||||
playlist_rows = (
|
||||
await session.execute(
|
||||
@@ -143,6 +240,9 @@ async def status_page(request: Request, session: AsyncSession = Depends(get_sess
|
||||
profiles=profiles,
|
||||
taste_profiles=taste_by_profile_id,
|
||||
recent_playlists=recent_playlists,
|
||||
feedback_summary=feedback_summary,
|
||||
recent_feedback=recent_feedback,
|
||||
vibe_influence=vibe_influence,
|
||||
now=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
|
||||
)
|
||||
return HTMLResponse(html)
|
||||
|
||||
Reference in New Issue
Block a user