tv shows to tv series

This commit is contained in:
2026-04-20 19:37:33 -03:00
parent c319dea2bf
commit 5e02ca267f
20 changed files with 396 additions and 117 deletions

277
CONTEXT.md Normal file
View File

@@ -0,0 +1,277 @@
# TV Show Recommendation System (Self-Hosted, Multi-Agent)
## 🎯 Purpose
This document provides **complete context for AI agents and developers** to build and evolve a local, self-hosted TV show recommendation system.
The system is designed to:
* Prioritize **current user intent** over historical behavior
* Use **multi-agent architecture**
* Generate, evaluate, and organize recommendations
* Handle **50100 candidate series per request**
---
# 🧠 Core Principles
1. **Context over history**
* Do NOT persist long-term taste profiles
* Every request must be evaluated independently
2. **Generation ≠ Evaluation**
* Candidate generation and ranking must be separated
3. **High recall → aggressive filtering**
* Generate many candidates (50100)
* Filter and rank later
4. **Structured communication between agents**
* All agents must exchange structured data (JSON-like)
5. **Deterministic where possible**
* Interpretation and ranking should be consistent
---
# 🏗️ System Overview
```
User Input
[1] Interpreter Agent
[2] Retrieval Agent
[3] Ranking Agent
[4] Curator Agent
```
---
# 1. 🧾 Interpreter Agent
## Goal
Convert raw user input into structured data.
## Input
Free-form user text describing:
* Liked series
* Disliked series
* Preferences (themes, tone, characters)
* Constraints (things to avoid)
## Output Schema
```json
{
"liked": ["string"],
"disliked": ["string"],
"themes": ["string"],
"character_preferences": ["string"],
"tone": ["string"],
"avoid": ["string"]
}
```
## Requirements
* Normalize terminology (e.g., "spy" → "espionage")
* Infer implicit preferences
* Detect contradictions
* Be deterministic (low temperature)
---
# 2. 🔎 Retrieval Agent
## Goal
Generate a **large and diverse pool (50100)** of candidate TV series.
## Strategy: LLM Generation
* Generate candidate series from structured input
* Focus on diversity and coverage (high LLM temperature)
## Output Schema
```json
[
{
"title": "string",
"metadata": {
"themes": [],
"tone": [],
"tags": []
}
}
]
```
## Constraints
* Do NOT use external APIs
* Favor recall over precision
---
# 3. ⚖️ Ranking Agent
## Goal
Categorize candidates into 4 confidence levels using relative comparison.
## Categories
* Definitely Like
* Might Like
* Questionable
* Will Not Like
## Method
### Step 1: Pre-filter
* Remove obvious mismatches
* Enforce "avoid" constraints
### Step 2: Pairwise Comparison
* Compare candidates relative to each other and input
### Step 3: Tagging
* Assign each show to one of the four categories
## Output Schema
```json
{
"definitely_like": ["title"],
"might_like": ["title"],
"questionable": ["title"],
"will_not_like": ["title"]
}
```
## Requirements
* Keep logic simple and consistent
* Be deterministic (low temperature)
---
# 4. 🎯 Curator Agent
## Goal
Produce a clean, user-facing recommendation output.
## Responsibilities
* Group series by category
* Provide short explanations
* Ensure readability
## Output Example
```
Definitely Like:
- Show A → reason
Might Like:
- Show B → reason
```
---
# 🔁 Feedback Loop (Re-Ranking Only)
## Goal
Improve ranking without storing long-term user preferences.
## Inputs
* External summaries, reviews, or insights
* User-provided feedback from other sources
## Behavior
* Adjust ranking dynamically
* Re-rank existing candidates
* Do NOT persist user taste
---
# ⚙️ Agent Configuration
## Temperature Guidelines
* Interpreter: Low
* Retrieval: Medium/High
* Ranking: Low
* Curator: Medium
## Rules
* Agents must be stateless
* Pass all required context explicitly
* No hidden memory
---
# 🧱 Project Structure
```
/agents
interpreter
retrieval
ranking
curator
/pipelines
recommendation_flow
```
---
# 🚫 Non-Goals
* No persistent taste model
* No long-term user profiling
---
# 🧭 Implementation Notes for AI Agents
When extending or modifying this system:
1. Do NOT introduce long-term memory of user preferences
2. Do NOT merge agent responsibilities
3. Always preserve structured input/output between agents
4. Prefer simplicity over overengineering
5. Ensure ranking remains interpretable and consistent
---
# ✅ Summary
This system is a **stateless, multi-agent recommendation pipeline** focused on:
* Strong alignment with current input
* High candidate diversity
* Structured filtering and ranking
* Scalable handling of large candidate sets
The architecture is intentionally simple, modular, and extensible.

View File

@@ -1,6 +1,6 @@
# Recommender
A pure TypeScript monolith AI agent application that will recommend TV shows based on a very customized user profile and input.
A pure TypeScript monolith AI agent application that will recommend TV series based on a very customized user profile and input.
## Project Structure

View File

@@ -10,8 +10,8 @@ CREATE TABLE IF NOT EXISTS "recommendations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"main_prompt" text NOT NULL,
"liked_shows" text DEFAULT '' NOT NULL,
"disliked_shows" text DEFAULT '' NOT NULL,
"liked_series" text DEFAULT '' NOT NULL,
"disliked_series" text DEFAULT '' NOT NULL,
"themes" text DEFAULT '' NOT NULL,
"brainstorm_count" integer DEFAULT 100 NOT NULL,
"recommendations" jsonb,

View File

@@ -0,0 +1,2 @@
ALTER TABLE "recommendations" RENAME COLUMN "liked_shows" TO "liked_series";
ALTER TABLE "recommendations" RENAME COLUMN "disliked_shows" TO "disliked_series";

View File

@@ -89,15 +89,15 @@
"primaryKey": false,
"notNull": true
},
"liked_shows": {
"name": "liked_shows",
"liked_series": {
"name": "liked_series",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"disliked_shows": {
"name": "disliked_shows",
"disliked_series": {
"name": "disliked_series",
"type": "text",
"primaryKey": false,
"notNull": true,

View File

@@ -4,7 +4,7 @@ import { z } from 'zod';
import { zodTextFormat } from 'openai/helpers/zod';
const CuratorSchema = z.object({
shows: z.array(z.object({
series: z.array(z.object({
title: z.string(),
explanation: z.string(),
category: z.enum(["Full Match", "Definitely Like", "Might Like", "Questionable", "Will Not Like"]),
@@ -24,7 +24,7 @@ export async function runCurator(
): Promise<CuratorOutput[]> {
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
const allShows = [
const allSeries = [
...(ranking.full_match ?? []).map((t) => ({ title: t, category: 'Full Match' as const })),
...ranking.definitely_like.map((t) => ({ title: t, category: 'Definitely Like' as const })),
...ranking.might_like.map((t) => ({ title: t, category: 'Might Like' as const })),
@@ -32,7 +32,7 @@ export async function runCurator(
...ranking.will_not_like.map((t) => ({ title: t, category: 'Will Not Like' as const })),
];
if (allShows.length === 0) return [];
if (allSeries.length === 0) return [];
const canSearch = useWebSearch && supportsWebSearch;
const preferenceSummary = `User preferences summary:
@@ -53,9 +53,9 @@ Rules:
- cons: up to 3 short bullet points about what the user might not like based on their preferences
- Be honest — explain why "Questionable" or "Will Not Like" ${mediaLabel}s got that rating`;
const chunks: typeof allShows[] = [];
for (let i = 0; i < allShows.length; i += CHUNK_SIZE) {
chunks.push(allShows.slice(i, i + CHUNK_SIZE));
const chunks: typeof allSeries[] = [];
for (let i = 0; i < allSeries.length; i += CHUNK_SIZE) {
chunks.push(allSeries.slice(i, i + CHUNK_SIZE));
}
const results: CuratorOutput[] = [];
@@ -66,14 +66,14 @@ Rules:
temperature: 0.5,
...serviceOptions,
...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
text: { format: zodTextFormat(CuratorSchema, "shows") },
text: { format: zodTextFormat(CuratorSchema, "series") },
instructions,
input: `${preferenceSummary}
${mediaLabel}s to describe:
${showList}`,
}));
results.push(...(response.output_parsed?.shows ?? []));
results.push(...(response.output_parsed?.series ?? []));
}
return results;

View File

@@ -15,8 +15,8 @@ const InterpreterSchema = z.object({
interface InterpreterInput {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
media_type: MediaType;
feedback_context?: string;
@@ -43,8 +43,8 @@ Rules:
- Be specific and concrete, not vague
- For "requirements": capture explicit hard requirements the user stated that recommendations must satisfy — things like "must be from the 2000s onward", "must have subtitles", "must feature a female lead". Leave empty if no such constraints were stated.`,
input: `Main prompt: ${input.main_prompt}
Liked ${mediaLabel}s: ${input.liked_shows || '(none)'}
Disliked ${mediaLabel}s: ${input.disliked_shows || '(none)'}
Liked ${mediaLabel}s: ${input.liked_series || '(none)'}
Disliked ${mediaLabel}s: ${input.disliked_series || '(none)'}
Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`,
}));

View File

@@ -19,7 +19,7 @@ export async function runRetrieval(
previousFullMatches: string[] = [],
): Promise<RetrievalOutput> {
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV series';
const canSearch = useWebSearch && supportsWebSearch;
const response = await parseWithRetry(() => openai.responses.parse({

View File

@@ -38,7 +38,7 @@ function buildSystemPrompt(
seenTitles: string[]
): string {
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV series';
let prompt = `You are a ${mediaLabel} recommendation specialist. Your task is to recommend titles that match the user's taste profile.

View File

@@ -5,8 +5,8 @@ export const recommendations = pgTable('recommendations', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
main_prompt: text('main_prompt').notNull(),
liked_shows: text('liked_shows').notNull().default(''),
disliked_shows: text('disliked_shows').notNull().default(''),
liked_series: text('liked_series').notNull().default(''),
disliked_series: text('disliked_series').notNull().default(''),
themes: text('themes').notNull().default(''),
brainstorm_count: integer('brainstorm_count').notNull().default(100),
media_type: text('media_type').notNull().default('tv_show'),

View File

@@ -39,8 +39,8 @@ function mergeCuratorOutputs(a: CuratorOutput[], b: CuratorOutput[]): CuratorOut
}
interface ContinuousPipelineInput {
likedShows: string;
dislikedShows?: string;
likedSeries: string;
dislikedSeries?: string;
themes?: string;
requirements?: string;
avoid?: string;
@@ -57,8 +57,8 @@ export async function runContinuousPipeline(
): Promise<CuratorOutput[]> {
const startTime = Date.now();
const {
likedShows,
dislikedShows = '',
likedSeries,
dislikedSeries = '',
themes = '',
requirements = '',
avoid = '',
@@ -83,9 +83,9 @@ export async function runContinuousPipeline(
const t0 = Date.now();
interpreterOutput = await runInterpreter({
main_prompt: themes || 'recommend shows based on user preferences',
liked_shows: likedShows,
disliked_shows: dislikedShows,
main_prompt: themes || 'recommend series based on user preferences',
liked_series: likedSeries,
disliked_series: dislikedSeries,
themes: themes,
media_type: mediaType,
});
@@ -267,8 +267,8 @@ export async function runContinuousPipeline(
.set({
title: aiTitle,
main_prompt: themes || 'Continuous recommendations',
liked_shows: likedShows,
disliked_shows: dislikedShows,
liked_series: likedSeries,
disliked_series: dislikedSeries,
themes: themes,
brainstorm_count: totalCount,
media_type: mediaType,

View File

@@ -201,8 +201,8 @@ export async function runPipeline(
const t0 = Date.now();
const interpreterOutput = await runInterpreter({
main_prompt: rec.main_prompt,
liked_shows: rec.liked_shows,
disliked_shows: rec.disliked_shows,
liked_series: rec.liked_series,
disliked_series: rec.disliked_series,
themes: rec.themes,
media_type: mediaType,
...(feedbackContext !== undefined ? { feedback_context: feedbackContext } : {}),

View File

@@ -29,8 +29,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
fastify.post('/recommendations', async (request, reply) => {
const body = request.body as {
main_prompt: string;
liked_shows?: string;
disliked_shows?: string;
liked_series?: string;
disliked_series?: string;
themes?: string;
brainstorm_count?: number;
media_type?: string;
@@ -64,8 +64,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
.values({
title: title || 'Untitled',
main_prompt: body.main_prompt ?? '',
liked_shows: body.liked_shows ?? '',
disliked_shows: body.disliked_shows ?? '',
liked_series: body.liked_series ?? '',
disliked_series: body.disliked_series ?? '',
themes: body.themes ?? '',
brainstorm_count,
media_type,
@@ -85,8 +85,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
// POST /recommendations/continuous — create record and run continuous pipeline with SSE
fastify.post('/recommendations/continuous', async (request, reply) => {
const body = request.body as {
liked_shows: string;
disliked_shows?: string;
liked_series: string;
disliked_series?: string;
themes?: string;
requirements?: string;
avoid?: string;
@@ -108,8 +108,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
.values({
title,
main_prompt: body.themes ?? 'Continuous recommendations',
liked_shows: body.liked_shows,
disliked_shows: body.disliked_shows ?? '',
liked_series: body.liked_series,
disliked_series: body.disliked_series ?? '',
themes: body.themes ?? '',
brainstorm_count: totalCount,
media_type: mediaType,
@@ -155,8 +155,8 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
// Run the continuous pipeline (it will now update the existing record)
await runContinuousPipeline(recId, {
likedShows: body.liked_shows,
dislikedShows: body.disliked_shows ?? '',
likedSeries: body.liked_series,
dislikedSeries: body.disliked_series ?? '',
themes: body.themes ?? '',
requirements: body.requirements ?? '',
avoid: body.avoid ?? '',
@@ -305,11 +305,11 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
const feedbackContext =
feedbackRows.length > 0
? feedbackRows
.map(
(f) =>
`${mediaLabel}: "${f.item_name}" — Rating: ${f.stars}/3 stars${f.feedback ? ` — Comment: ${f.feedback}` : ''}`,
)
.join('\n')
.map(
(f) =>
`${mediaLabel}: "${f.item_name}" — Rating: ${f.stars}/3 stars${f.feedback ? ` — Comment: ${f.feedback}` : ''}`,
)
.join('\n')
: undefined;
await runPipeline(rec, (event) => {

View File

@@ -83,8 +83,8 @@ export interface ContinuousSession {
export interface ContinuousStartRequest {
mediaType: 'tv_show' | 'movie';
likedShows: string;
dislikedShows?: string;
likedSeries: string;
dislikedSeries?: string;
themes?: string;
requirements?: string;
avoid?: string;

View File

@@ -18,8 +18,8 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
export function createRecommendation(body: {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
brainstorm_count?: number;
media_type: MediaType;
@@ -68,8 +68,8 @@ export function deleteRecommendation(id: string): Promise<{ ok: boolean }> {
}
export function createContinuousRecommendation(body: {
liked_shows: string;
disliked_shows?: string;
liked_series: string;
disliked_series?: string;
themes?: string;
requirements?: string;
avoid?: string;

View File

@@ -8,8 +8,8 @@ interface NewRecommendationModalProps {
onClose: () => void;
onSubmit: (body: {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
requirements?: string;
avoid?: string;
@@ -33,19 +33,19 @@ const MEDIA_OPTIONS: Array<{
label: string;
description: string;
}> = [
{
type: 'tv_show',
icon: '📺',
label: 'TV Shows',
description: 'Serialized stories, limited series, and long-form comfort watches.',
},
{
type: 'movie',
icon: '🎬',
label: 'Movies',
description: 'Feature films, prestige cinema, and one-night picks.',
},
];
{
type: 'tv_show',
icon: '📺',
label: 'TV series',
description: 'Serialized stories, limited series, and long-form comfort watches.',
},
{
type: 'movie',
icon: '🎬',
label: 'Movies',
description: 'Feature films, prestige cinema, and one-night picks.',
},
];
const MODE_OPTIONS: Array<{
mode: GenerationMode;
@@ -53,27 +53,27 @@ const MODE_OPTIONS: Array<{
badge: string;
description: string;
}> = [
{
mode: 'brainstorm',
label: 'Brainstorm',
badge: 'Best for variety',
description: 'Explore a broad pool of options, then rank and curate the strongest fits.',
},
{
mode: 'continuous',
label: 'Continuous',
badge: 'Best for deep search',
description: 'Generate recommendations in chained batches for a steadier, longer-running hunt.',
},
];
{
mode: 'brainstorm',
label: 'Brainstorm',
badge: 'Best for variety',
description: 'Explore a broad pool of options, then rank and curate the strongest fits.',
},
{
mode: 'continuous',
label: 'Continuous',
badge: 'Best for deep search',
description: 'Generate recommendations in chained batches for a steadier, longer-running hunt.',
},
];
export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationModalProps) {
const [step, setStep] = useState<'type' | 'mode' | 'form'>('type');
const [mediaType, setMediaType] = useState<MediaType>('tv_show');
const [generationMode, setGenerationMode] = useState<GenerationMode>('brainstorm');
const [mainPrompt, setMainPrompt] = useState('');
const [likedShows, setLikedShows] = useState('');
const [dislikedShows, setDislikedShows] = useState('');
const [likedSeries, setLikedSeries] = useState('');
const [dislikedSeries, setDislikedSeries] = useState('');
const [themes, setThemes] = useState('');
const [requirements, setRequirements] = useState('');
const [avoid, setAvoid] = useState('');
@@ -100,7 +100,7 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
}, [loading, onClose]);
const mediaLabel = mediaType === 'movie' ? 'Movie' : 'TV Show';
const mediaPluralLabel = mediaType === 'movie' ? 'movies' : 'shows';
const mediaPluralLabel = mediaType === 'movie' ? 'movies' : 'series';
const handleSelectType = (type: MediaType) => {
setMediaType(type);
@@ -119,15 +119,15 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (generationMode === 'brainstorm' && !mainPrompt.trim()) return;
if (!likedShows.trim()) return;
if (!likedSeries.trim()) return;
setLoading(true);
try {
if (generationMode === 'brainstorm') {
await onSubmit({
main_prompt: mainPrompt.trim(),
liked_shows: likedShows.trim(),
disliked_shows: dislikedShows.trim(),
liked_series: likedSeries.trim(),
disliked_series: dislikedSeries.trim(),
themes: themes.trim(),
brainstorm_count: brainstormCount,
media_type: mediaType,
@@ -142,8 +142,8 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
} else {
await onSubmit({
main_prompt: '',
liked_shows: likedShows.trim(),
disliked_shows: dislikedShows.trim(),
liked_series: likedSeries.trim(),
disliked_series: dislikedSeries.trim(),
themes: themes.trim(),
requirements: requirements.trim(),
avoid: avoid.trim(),
@@ -258,7 +258,7 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
<div class="modal-type-footer">
<div class="modal-selection-summary">
<span class="summary-pill">{mediaType === 'movie' ? 'Movies' : 'TV Shows'}</span>
<span class="summary-pill">{mediaType === 'movie' ? 'Movies' : 'TV series'}</span>
<span class="summary-pill">{selectedMode?.label}</span>
</div>
</div>
@@ -281,7 +281,7 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
<form class="modal-form" onSubmit={handleSubmit}>
<div class="modal-summary-strip">
<span class="summary-pill">{mediaType === 'movie' ? 'Movies' : 'TV Shows'}</span>
<span class="summary-pill">{mediaType === 'movie' ? 'Movies' : 'TV series'}</span>
<span class="summary-pill summary-pill--accent">{selectedMode?.label}</span>
<span class="summary-caption">
{generationMode === 'brainstorm'
@@ -307,28 +307,28 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
<div class="modal-form-grid">
<div class="form-group">
<label for="liked-shows">{mediaLabel}s you liked</label>
<label for="liked-series">{mediaLabel}s you liked</label>
<input
id="liked-shows"
id="liked-series"
type="text"
class="form-input"
placeholder={mediaType === 'movie' ? 'e.g. Inception, The Godfather' : 'e.g. Breaking Bad, The Wire'}
value={likedShows}
onInput={(e) => setLikedShows((e.target as HTMLInputElement).value)}
value={likedSeries}
onInput={(e) => setLikedSeries((e.target as HTMLInputElement).value)}
required
/>
<span class="form-help">A few strong examples help the pipeline lock onto your taste.</span>
</div>
<div class="form-group">
<label for="disliked-shows">{mediaLabel}s you disliked</label>
<label for="disliked-series">{mediaLabel}s you disliked</label>
<input
id="disliked-shows"
id="disliked-series"
type="text"
class="form-input"
placeholder={mediaType === 'movie' ? 'e.g. Transformers' : 'e.g. Game of Thrones'}
value={dislikedShows}
onInput={(e) => setDislikedShows((e.target as HTMLInputElement).value)}
value={dislikedSeries}
onInput={(e) => setDislikedSeries((e.target as HTMLInputElement).value)}
/>
<span class="form-help">Optional, but useful when you want to steer away from common misses.</span>
</div>

View File

@@ -14,8 +14,8 @@ type GenerationMode = 'brainstorm' | 'continuous';
interface CreateBody {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
requirements?: string;
avoid?: string;
@@ -58,8 +58,8 @@ export function useRecommendations() {
if (body.generation_mode === 'continuous') {
const result = await createContinuousRecommendation({
liked_shows: body.liked_shows,
disliked_shows: body.disliked_shows,
liked_series: body.liked_series,
disliked_series: body.disliked_series,
themes: body.themes,
requirements: body.requirements ?? '',
avoid: body.avoid ?? '',
@@ -73,7 +73,7 @@ export function useRecommendations() {
const result = await createRecommendation(body);
id = result.id;
}
await refreshList();
setSelectedId(id);
return id;

View File

@@ -13,8 +13,8 @@ export function Home() {
const handleCreateNew = async (body: {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
requirements?: string;
avoid?: string;

View File

@@ -183,8 +183,8 @@ export function Recom({ id }: RecomProps) {
const handleCreateNew = async (body: {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
brainstorm_count?: number;
media_type: import('../types/index.js').MediaType;
@@ -234,16 +234,16 @@ export function Recom({ id }: RecomProps) {
<span class="rec-info-value">{rec.main_prompt}</span>
</div>
)}
{rec.liked_shows && (
{rec.liked_series && (
<div class="rec-info-row">
<span class="rec-info-label">Liked</span>
<span class="rec-info-value">{rec.liked_shows}</span>
<span class="rec-info-value">{rec.liked_series}</span>
</div>
)}
{rec.disliked_shows && (
{rec.disliked_series && (
<div class="rec-info-row">
<span class="rec-info-label">Disliked</span>
<span class="rec-info-value">{rec.disliked_shows}</span>
<span class="rec-info-value">{rec.disliked_series}</span>
</div>
)}
{rec.themes && (
@@ -254,7 +254,7 @@ export function Recom({ id }: RecomProps) {
)}
<div class="rec-info-row">
<span class="rec-info-label">Media</span>
<span class="rec-info-value">{rec.media_type === 'tv_show' ? 'TV Shows' : 'Movies'}</span>
<span class="rec-info-value">{rec.media_type === 'tv_show' ? 'TV series' : 'Movies'}</span>
</div>
{rec.use_web_search && (
<div class="rec-info-row">

View File

@@ -17,8 +17,8 @@ export interface Recommendation {
id: string;
title: string;
main_prompt: string;
liked_shows: string;
disliked_shows: string;
liked_series: string;
disliked_series: string;
themes: string;
media_type: MediaType;
use_web_search: boolean;