Expand taste profile: show up to 60 tags with score bars

Top 10 shown as variable-size tag cloud, all tags below as a
two-column bar chart. Backend limit raised from 20 to 60.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 00:15:57 +02:00
parent 32e55b9042
commit 15e6b94cbf
2 changed files with 67 additions and 31 deletions

View File

@@ -114,7 +114,7 @@ def get_stats(
SELECT tag, score FROM user_tag_affinity SELECT tag, score FROM user_tag_affinity
WHERE user_id = :uid AND score > 0 WHERE user_id = :uid AND score > 0
ORDER BY score DESC ORDER BY score DESC
LIMIT 20 LIMIT 60
"""), """),
{"uid": uid}, {"uid": uid},
).mappings().all() ).mappings().all()

View File

@@ -62,8 +62,8 @@ export default function Stats() {
const maxDayCount = Math.max(...days.map(d => dailyMap[d]?.count || 0), 1); const maxDayCount = Math.max(...days.map(d => dailyMap[d]?.count || 0), 1);
const maxCatCount = Math.max(...(data.top_categories || []).map(c => c.watch_count || 0), 1); const maxCatCount = Math.max(...(data.top_categories || []).map(c => c.watch_count || 0), 1);
const topTags = (data.taste_profile || []).slice(0, 12); const allTags = data.taste_profile || [];
const maxTagScore = Math.max(...topTags.map(t => t.score || 0), 1); const maxTagScore = Math.max(...allTags.map(t => t.score || 0), 1);
return ( return (
<div className="flex flex-col gap-8 max-w-4xl mx-auto"> <div className="flex flex-col gap-8 max-w-4xl mx-auto">
@@ -235,40 +235,76 @@ export default function Stats() {
})()} })()}
{/* Taste profile */} {/* Taste profile */}
{topTags.length > 0 && ( {allTags.length > 0 && (
<div className="bg-zinc-900 rounded-2xl p-5 flex flex-col gap-3"> <div className="bg-zinc-900 rounded-2xl p-5 flex flex-col gap-4">
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<h2 className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Taste profile</h2> <h2 className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Taste profile</h2>
<p className="text-[11px] text-zinc-600">built from your watches, likes and bookmarks</p> <p className="text-[11px] text-zinc-600">{allTags.length} interests · built from watches, likes &amp; bookmarks</p>
</div> </div>
{/* Top tier tag cloud */}
<div>
<p className="text-[10px] text-zinc-600 uppercase tracking-wider mb-2">Top interests</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{topTags.map(t => { {allTags.slice(0, 10).map(t => {
const intensity = t.score / maxTagScore; const intensity = t.score / maxTagScore;
return ( return (
<span <span
key={t.tag} key={t.tag}
title={`score: ${t.score.toFixed(1)}`} title={`score: ${t.score.toFixed(1)}`}
className="flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium" className="flex items-center gap-1 px-3 py-1 rounded-full font-semibold"
style={{ style={{
backgroundColor: `rgba(250,204,21,${0.08 + intensity * 0.18})`, backgroundColor: `rgba(250,204,21,${0.1 + intensity * 0.2})`,
color: `hsl(50,95%,${55 + intensity * 20}%)`, color: `hsl(50,95%,${55 + intensity * 20}%)`,
fontSize: `${11 + intensity * 4}px`, fontSize: `${12 + intensity * 5}px`,
}} }}
> >
{t.tag} {t.tag}
<button <button
onClick={() => deleteTag.mutate(t.tag)} onClick={() => deleteTag.mutate(t.tag)}
disabled={deleteTag.isPending} disabled={deleteTag.isPending}
className="opacity-40 hover:opacity-100 transition-opacity leading-none ml-0.5" className="opacity-30 hover:opacity-100 transition-opacity leading-none ml-0.5"
title="Remove from taste profile" title="Remove"
> >×</button>
×
</button>
</span> </span>
); );
})} })}
</div> </div>
</div> </div>
{/* All tags as score bars */}
{allTags.length > 10 && (
<div>
<p className="text-[10px] text-zinc-600 uppercase tracking-wider mb-3">All interests</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2">
{allTags.map(t => {
const pct = (t.score / maxTagScore) * 100;
const intensity = t.score / maxTagScore;
return (
<div key={t.tag} className="flex items-center gap-2 group/tag">
<span className="text-[12px] text-zinc-300 w-32 shrink-0 truncate">{t.tag}</span>
<div className="flex-1 h-1.5 bg-zinc-800 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{
width: `${pct}%`,
backgroundColor: `hsl(50,95%,${40 + intensity * 25}%)`,
}}
/>
</div>
<button
onClick={() => deleteTag.mutate(t.tag)}
disabled={deleteTag.isPending}
className="opacity-0 group-hover/tag:opacity-40 hover:!opacity-100 transition-opacity text-zinc-400 text-xs leading-none shrink-0"
title="Remove"
>×</button>
</div>
);
})}
</div>
</div>
)}
</div>
)} )}
</div> </div>
); );