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
WHERE user_id = :uid AND score > 0
ORDER BY score DESC
LIMIT 20
LIMIT 60
"""),
{"uid": uid},
).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 maxCatCount = Math.max(...(data.top_categories || []).map(c => c.watch_count || 0), 1);
const topTags = (data.taste_profile || []).slice(0, 12);
const maxTagScore = Math.max(...topTags.map(t => t.score || 0), 1);
const allTags = data.taste_profile || [];
const maxTagScore = Math.max(...allTags.map(t => t.score || 0), 1);
return (
<div className="flex flex-col gap-8 max-w-4xl mx-auto">
@@ -235,40 +235,76 @@ export default function Stats() {
})()}
{/* Taste profile */}
{topTags.length > 0 && (
<div className="bg-zinc-900 rounded-2xl p-5 flex flex-col gap-3">
{allTags.length > 0 && (
<div className="bg-zinc-900 rounded-2xl p-5 flex flex-col gap-4">
<div className="flex items-baseline gap-2">
<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>
{/* 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">
{topTags.map(t => {
{allTags.slice(0, 10).map(t => {
const intensity = t.score / maxTagScore;
return (
<span
key={t.tag}
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={{
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}%)`,
fontSize: `${11 + intensity * 4}px`,
fontSize: `${12 + intensity * 5}px`,
}}
>
{t.tag}
<button
onClick={() => deleteTag.mutate(t.tag)}
disabled={deleteTag.isPending}
className="opacity-40 hover:opacity-100 transition-opacity leading-none ml-0.5"
title="Remove from taste profile"
>
×
</button>
className="opacity-30 hover:opacity-100 transition-opacity leading-none ml-0.5"
title="Remove"
>×</button>
</span>
);
})}
</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>
);