feat: add top pick for you section to homepage
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
# Recommendation Architecture
|
||||
|
||||
This document defines the long-term shape of the `For You` discovery system.
|
||||
This document defines the long-term shape of the `Top Pick for You`
|
||||
recommendation system.
|
||||
The goal is to keep the current implementation simple enough to operate inside
|
||||
the existing Go application while preserving a clean path toward a larger
|
||||
recommender system.
|
||||
|
||||
## Current Serving Model
|
||||
|
||||
The current `For You` implementation is a bounded hybrid ranker:
|
||||
The current `Top Pick for You` implementation is a bounded hybrid ranker:
|
||||
|
||||
- builds weighted seeds from user watch history
|
||||
- uses Jikan recommendation edges as collaborative candidates
|
||||
- uses watchlist-derived genres, themes, studios, and demographics as profile
|
||||
search candidates
|
||||
- excludes anime already present in the watchlist
|
||||
- boosts candidates that match user taste signals
|
||||
- reranks the final list to reduce genre pileups
|
||||
@@ -19,10 +22,11 @@ The online request path stays intentionally small:
|
||||
|
||||
1. load recent watchlist state
|
||||
2. derive strong seeds
|
||||
3. fetch bounded candidate set
|
||||
4. score candidates
|
||||
5. rerank for diversity
|
||||
6. return top results
|
||||
3. build a weighted taste profile from those seeds
|
||||
4. fetch bounded collaborative and profile-search candidate sets
|
||||
5. score candidates
|
||||
6. rerank for diversity
|
||||
7. return top results
|
||||
|
||||
## Target System Shape
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
isJstTimezone,
|
||||
normalizeWeekday,
|
||||
parseHHMM,
|
||||
} from "./shared/broadcast";
|
||||
import { isJstTimezone, normalizeWeekday, parseHHMM } from "./shared/broadcast";
|
||||
|
||||
export {};
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
</template>
|
||||
<script type="module" src="/dist/static/dropdown.js" defer></script>
|
||||
<script type="module" src="/dist/static/discover.js" defer></script>
|
||||
<script type="module" src="/dist/static/top_pick_carousel.js" defer></script>
|
||||
<script type="module" src="/dist/static/anime.js" defer></script>
|
||||
<script type="module" src="/dist/static/timezone.js" defer></script>
|
||||
<script type="module" src="/dist/static/player/main.js" defer></script>
|
||||
|
||||
@@ -34,17 +34,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* For You Section */}}
|
||||
<section id="discover-for-you-section" class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-base font-normal text-foreground">For You</h2>
|
||||
<span class="text-sm text-foreground-muted">Based on your watchlist</span>
|
||||
</div>
|
||||
<div hx-get="/api/discover/for-you" hx-trigger="load" hx-target="#discover-for-you-section" hx-swap="outerHTML">
|
||||
{{template "discover_skeleton"}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* Upcoming Section */}}
|
||||
<section class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -85,30 +74,6 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "discover_row"}}
|
||||
<div class="grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(160px,1fr))]">
|
||||
{{range .Animes}}
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "discover_for_you_section"}}
|
||||
{{if gt (len .Animes) 0}}
|
||||
<section id="discover-for-you-section" class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-base font-normal text-foreground">For You</h2>
|
||||
<span class="text-sm text-foreground-muted">Based on your watchlist</span>
|
||||
</div>
|
||||
<div class="grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(160px,1fr))]">
|
||||
{{range .Animes}}
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "discover_skeleton"}}
|
||||
<div class="discover-grid">
|
||||
{{range (seq 8)}}
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
<div hx-get="/api/catalog/continue" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{template "continue_watching_skeleton"}}
|
||||
</div>
|
||||
|
||||
<div hx-get="/api/catalog/top-pick" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{template "top_pick_for_you_skeleton"}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<section class="w-full">
|
||||
@@ -36,6 +40,37 @@
|
||||
|
||||
{{end}}
|
||||
|
||||
{{define "top_pick_for_you_section"}}
|
||||
{{if gt (len .Animes) 0}}
|
||||
<section id="top-pick-for-you-section" class="w-full" data-top-pick-carousel>
|
||||
<div class="mb-4 flex items-end justify-between gap-3">
|
||||
<h2 class="min-w-0 text-base font-normal text-foreground">Top Pick for You</h2>
|
||||
<a href="/discover" class="group flex items-center gap-1 text-sm text-foreground-muted transition-colors hover:text-foreground">
|
||||
Explore more
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-4 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="relative overflow-hidden">
|
||||
<div data-top-pick-track tabindex="0" class="grid min-w-0 grid-flow-col auto-cols-[calc((100%_-_0.75rem)/2.18)] gap-3 overflow-x-auto scroll-smooth pb-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden md:auto-cols-[calc((100%_-_2rem)/3.35)] md:gap-4 lg:auto-cols-[calc((100%_-_3rem)/4.35)] xl:auto-cols-[calc((100%_-_4rem)/5.35)] 2xl:auto-cols-[calc((100%_-_5rem)/6.35)]">
|
||||
{{range .Animes}}
|
||||
<div data-top-pick-item>
|
||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div data-top-pick-prev-fade class="pointer-events-none absolute inset-y-0 left-0 z-[70] hidden w-20 bg-linear-to-r from-background via-background/80 to-transparent" aria-hidden="true"></div>
|
||||
<div data-top-pick-next-fade class="pointer-events-none absolute inset-y-0 right-0 z-[70] w-20 bg-linear-to-l from-background via-background/80 to-transparent" aria-hidden="true"></div>
|
||||
<button type="button" data-unstyled-button data-top-pick-prev class="pointer-events-auto absolute left-0 top-[42%] z-[80] hidden h-16 w-14 -translate-y-1/2 cursor-pointer items-center justify-center text-foreground-muted transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent" aria-label="Scroll top picks left" aria-hidden="true">
|
||||
<svg class="pointer-events-auto size-10 drop-shadow-[0_1px_2px_rgba(0,0,0,0.45)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||||
</button>
|
||||
<button type="button" data-unstyled-button data-top-pick-next class="pointer-events-auto absolute right-0 top-[42%] z-[80] inline-flex h-16 w-14 -translate-y-1/2 cursor-pointer items-center justify-center text-foreground-muted transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent" aria-label="Scroll top picks right" aria-hidden="false">
|
||||
<svg class="pointer-events-auto size-10 drop-shadow-[0_1px_2px_rgba(0,0,0,0.45)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "catalog_section"}}
|
||||
{{if eq .Section "Continue"}}
|
||||
{{if .ContinueWatching}}
|
||||
@@ -61,6 +96,26 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "top_pick_for_you_skeleton"}}
|
||||
<section id="top-pick-for-you-section" class="w-full">
|
||||
<div class="mb-4 flex items-end justify-between gap-3">
|
||||
<div class="skeleton h-5 w-36"></div>
|
||||
<div class="skeleton skeleton-subtle h-4 w-24"></div>
|
||||
</div>
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="grid min-w-0 grid-flow-col auto-cols-[calc((100%_-_0.75rem)/2.18)] gap-3 overflow-hidden pb-1 md:auto-cols-[calc((100%_-_2rem)/3.35)] md:gap-4 lg:auto-cols-[calc((100%_-_3rem)/4.35)] xl:auto-cols-[calc((100%_-_4rem)/5.35)] 2xl:auto-cols-[calc((100%_-_5rem)/6.35)]">
|
||||
{{range (seq 6)}}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="skeleton aspect-2/3 w-full"></div>
|
||||
<div class="skeleton h-4 w-3/4"></div>
|
||||
<div class="skeleton skeleton-subtle h-3 w-1/2"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{define "continue_watching_skeleton"}}
|
||||
<section id="continue-watching-section" class="w-full">
|
||||
<h2 class="mb-3 text-base font-normal text-foreground">Continue Watching</h2>
|
||||
|
||||
Reference in New Issue
Block a user