tv shows to tv series
This commit is contained in:
277
CONTEXT.md
Normal file
277
CONTEXT.md
Normal 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 **50–100 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 (50–100)
|
||||
* 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 (50–100)** 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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
packages/backend/drizzle/0003_changing_tv_series.sql
Normal file
2
packages/backend/drizzle/0003_changing_tv_series.sql
Normal 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";
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -36,7 +36,7 @@ const MEDIA_OPTIONS: Array<{
|
||||
{
|
||||
type: 'tv_show',
|
||||
icon: '📺',
|
||||
label: 'TV Shows',
|
||||
label: 'TV series',
|
||||
description: 'Serialized stories, limited series, and long-form comfort watches.',
|
||||
},
|
||||
{
|
||||
@@ -72,8 +72,8 @@ export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationM
|
||||
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>
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user