feat: surface recommendation rationale
This commit is contained in:
@@ -24,7 +24,7 @@ func rerankRecommendationCandidates(candidates []recommendationCandidate, limit
|
||||
continue
|
||||
}
|
||||
|
||||
selected = append(selected, domain.Anime{Anime: candidate.anime})
|
||||
selected = append(selected, domain.Anime{Anime: candidate.anime, RecommendationRationale: candidate.rationale})
|
||||
features := diversityFeatures(candidate.anime)
|
||||
seen.add(features)
|
||||
recent = append(recent, features)
|
||||
|
||||
@@ -48,9 +48,23 @@ func scoreRecommendationCandidate(
|
||||
themeMatches: themes,
|
||||
studioMatches: studios,
|
||||
demographicMatches: demos,
|
||||
rationale: buildRecommendationRationale(profile, candidate),
|
||||
}
|
||||
}
|
||||
|
||||
func buildRecommendationRationale(profile userTasteProfile, candidate jikan.Anime) []string {
|
||||
rationale := make([]string, 0, 4)
|
||||
rationale = append(rationale, matchedEntityNames(profile.genres, candidate.Genres)...)
|
||||
rationale = append(rationale, matchedEntityNames(profile.themes, candidate.Themes)...)
|
||||
rationale = append(rationale, matchedEntityNames(profile.studios, candidate.Studios)...)
|
||||
rationale = append(rationale, matchedEntityNames(profile.demographics, candidate.Demographics)...)
|
||||
|
||||
if len(rationale) > 4 {
|
||||
return rationale[:4]
|
||||
}
|
||||
return rationale
|
||||
}
|
||||
|
||||
func recommendationCandidateScoreAdjustments(now time.Time, profile userTasteProfile, candidate jikan.Anime) float64 {
|
||||
var score float64
|
||||
|
||||
@@ -115,3 +129,22 @@ func weightedEntityMatch(weights map[int]float64, entities []jikan.NamedEntity)
|
||||
|
||||
return matches, score
|
||||
}
|
||||
|
||||
func matchedEntityNames(weights map[int]float64, entities []jikan.NamedEntity) []string {
|
||||
if len(weights) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
names := make([]string, 0, 1)
|
||||
for _, entity := range entities {
|
||||
if entity.Name == "" || weights[entity.MalID] <= 0 {
|
||||
continue
|
||||
}
|
||||
names = append(names, entity.Name)
|
||||
if len(names) >= 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ type recommendationCandidate struct {
|
||||
themeMatches int
|
||||
studioMatches int
|
||||
demographicMatches int
|
||||
rationale []string
|
||||
}
|
||||
|
||||
type userTasteProfile struct {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
type Anime struct {
|
||||
jikan.Anime
|
||||
RecommendationRationale []string
|
||||
}
|
||||
|
||||
type Genre struct {
|
||||
|
||||
@@ -21,7 +21,17 @@
|
||||
{{else}}
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7">
|
||||
{{range .Animes}}
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
<div class="flex flex-col gap-2">
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "HideTitle" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
<h3 class="line-clamp-2 text-sm font-normal text-foreground">{{.DisplayTitle}}</h3>
|
||||
{{if .RecommendationRationale}}
|
||||
<div class="flex flex-wrap gap-1.5" aria-label="Why this was picked">
|
||||
{{range .RecommendationRationale}}
|
||||
<span class="rounded-full bg-background-button px-2 py-1 text-[0.68rem] leading-none text-foreground-muted">{{.}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user