- {visibleCards.map((type) => (
+
+ {CARD_TYPES.map((type) => (
);
-}
\ No newline at end of file
+}
diff --git a/components/cards/CardContent.tsx b/components/cards/CardContent.tsx
index d9246ec..54ce8e9 100644
--- a/components/cards/CardContent.tsx
+++ b/components/cards/CardContent.tsx
@@ -2,12 +2,8 @@
import { useEffect, useState } from "react";
import type { CardType } from "@/lib/cards";
-import { HistoricalAccuracyView } from "./HistoricalAccuracy";
import { InternationalRatingsView } from "./InternationalRatings";
-import { ProductionFactsView } from "./ProductionFacts";
import { RatingExplainedView } from "./RatingExplained";
-import { SensitivityGuideView } from "./SensitivityGuide";
-import { SkipGuideView } from "./SkipGuide";
interface CardContentProps {
movieId: number;
@@ -99,17 +95,9 @@ export function CardContent({ movieId, cardType }: CardContentProps) {
switch (cardType) {
case "rating_explained":
return
;
- case "skip_guide":
- return
;
- case "sensitivity_guide":
- return
;
case "international_ratings":
return
;
- case "production_facts":
- return
;
- case "historical_accuracy":
- return
;
default:
return null;
}
-}
\ No newline at end of file
+}
diff --git a/components/cards/HistoricalAccuracy.tsx b/components/cards/HistoricalAccuracy.tsx
deleted file mode 100644
index 45d3283..0000000
--- a/components/cards/HistoricalAccuracy.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import {
- historicalAccuracySchema,
- type HistoricalAccuracy,
-} from "@/lib/schemas";
-
-export function HistoricalAccuracyView({ data }: { data: unknown }) {
- const result = historicalAccuracySchema.safeParse(data);
- if (!result.success) {
- return (
-
- Could not display this insight (data format issue). Try another card or refresh later.
-
- );
- }
- const parsed = result.data as HistoricalAccuracy;
-
- return (
-
-
-
-
- Accuracy score
-
-
- {parsed.accuracyScore}
- /100
-
-
-
- Confidence: {parsed.confidence}
-
-
-
-
{parsed.summary}
-
- {parsed.accurateElements.length > 0 && (
-
-
What holds up
-
- {parsed.accurateElements.map((item) => (
- -
- {item}
-
- ))}
-
-
- )}
-
- {parsed.inventions.length > 0 && (
-
-
- Dramatizations & inventions
-
-
- {parsed.inventions.map((item, index) => (
-
-
- Film claim: {item.claim}
-
-
- Reality: {item.reality}
-
-
- ))}
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/components/cards/ProductionFacts.tsx b/components/cards/ProductionFacts.tsx
deleted file mode 100644
index 259fbe4..0000000
--- a/components/cards/ProductionFacts.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import {
- productionFactsSchema,
- type ProductionFacts,
-} from "@/lib/schemas";
-
-export function ProductionFactsView({ data }: { data: unknown }) {
- const result = productionFactsSchema.safeParse(data);
- if (!result.success) {
- return (
-
- Could not display this insight (data format issue). Try another card or refresh later.
-
- );
- }
- const parsed = result.data as ProductionFacts;
-
- return (
-
- {(parsed.budget || parsed.boxOffice) && (
-
- {parsed.budget ? : null}
- {parsed.boxOffice ? (
-
- ) : null}
-
- )}
-
- {parsed.filmingLocations?.length ? (
-
-
Filming locations
-
- {parsed.filmingLocations.map((location) => (
-
- {location}
-
- ))}
-
-
- ) : null}
-
-
- {parsed.facts.map((item, index) => (
-
-
- {item.category}
-
-
- {item.fact}
-
-
- ))}
-
-
- );
-}
-
-function Stat({ label, value }: { label: string; value: string }) {
- return (
-
- );
-}
\ No newline at end of file
diff --git a/components/cards/SensitivityGuide.tsx b/components/cards/SensitivityGuide.tsx
deleted file mode 100644
index 444875f..0000000
--- a/components/cards/SensitivityGuide.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import {
- sensitivityGuideSchema,
- type SensitivityGuide,
-} from "@/lib/schemas";
-import { cn } from "@/lib/utils";
-
-const severityColors = {
- mild: "border-emerald-400/20 bg-emerald-400/5",
- moderate: "border-amber-400/20 bg-amber-400/5",
- strong: "border-red-400/20 bg-red-400/5",
-};
-
-export function SensitivityGuideView({ data }: { data: unknown }) {
- const result = sensitivityGuideSchema.safeParse(data);
- if (!result.success) {
- return (
-
- Could not display this insight (data format issue). Try another card or refresh later.
-
- );
- }
- const parsed = result.data as SensitivityGuide;
-
- return (
-
-
{parsed.summary}
-
-
- {parsed.themes.map((theme, index) => (
-
-
-
{theme.theme}
-
- {theme.severity}
-
- {theme.approximateTiming ? (
-
- ~{theme.approximateTiming}
-
- ) : null}
-
-
- {theme.description}
-
-
- ))}
-
-
- );
-}
\ No newline at end of file
diff --git a/components/cards/SkipGuide.tsx b/components/cards/SkipGuide.tsx
deleted file mode 100644
index 9422734..0000000
--- a/components/cards/SkipGuide.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import { skipGuideSchema, type SkipGuide } from "@/lib/schemas";
-import { cn } from "@/lib/utils";
-
-const recommendationStyles = {
- recommended: "bg-red-100 text-red-700 border-red-200",
- optional: "bg-amber-100 text-amber-700 border-amber-200",
- do_not_skip: "bg-stone-200 text-stone-700 border-stone-300",
-};
-
-const recommendationLabels = {
- recommended: "Recommended skip",
- optional: "Optional skip",
- do_not_skip: "Do not skip",
-};
-
-function parseTimestamp(value: string): number {
- const hours = value.match(/(\d+)\s*h/i);
- const minutes = value.match(/(\d+)\s*m/i);
- return (hours ? Number(hours[1]) * 60 : 0) + (minutes ? Number(minutes[1]) : 0);
-}
-
-export function SkipGuideView({ data }: { data: unknown }) {
- const result = skipGuideSchema.safeParse(data);
- if (!result.success) {
- return (
-
- Could not display this insight (data format issue). Try another card or refresh later.
-
- );
- }
- const parsed = result.data as SkipGuide;
- const maxMinutes = Math.max(
- ...parsed.segments.map((segment) => parseTimestamp(segment.end)),
- 120,
- );
-
- return (
-
-
-
-
-
-
-
-
-
Content timeline
-
- {parsed.segments.map((segment, index) => {
- const start = parseTimestamp(segment.start);
- const end = parseTimestamp(segment.end);
- const left = (start / maxMinutes) * 100;
- const width = Math.max(((end - start) / maxMinutes) * 100, 1.5);
-
- return (
-
- );
- })}
-
-
- 0:00
- {Math.floor(maxMinutes / 60)}h {maxMinutes % 60}m
-
-
-
-
- {parsed.segments.map((segment, index) => (
-
-
-
- {segment.start} – {segment.end}
-
-
- {recommendationLabels[segment.skipRecommendation]}
-
-
- Plot impact: {segment.plotImpact.replace("_", " ")}
-
-
-
{segment.reason}
-
- Resume hint:{" "}
- {segment.resumeHint}
-
-
- ))}
-
-
-
{parsed.disclaimer}
-
- );
-}
-
-function Stat({ label, value }: { label: string; value: string }) {
- return (
-
- );
-}
\ No newline at end of file
diff --git a/karpathy.md b/karpathy.md
new file mode 100644
index 0000000..05ddc06
--- /dev/null
+++ b/karpathy.md
@@ -0,0 +1,59 @@
+Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
+
+**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
+
+## 1. Think Before Coding
+
+**Don't assume. Don't hide confusion. Surface tradeoffs.**
+
+Before implementing:
+- State your assumptions explicitly. If uncertain, ask.
+- If multiple interpretations exist, present them - don't pick silently.
+- If a simpler approach exists, say so. Push back when warranted.
+- If something is unclear, stop. Name what's confusing. Ask.
+
+## 2. Simplicity First
+
+**Minimum code that solves the problem. Nothing speculative.**
+
+- No features beyond what was asked.
+- No abstractions for single-use code.
+- No "flexibility" or "configurability" that wasn't requested.
+- No error handling for impossible scenarios.
+- If you write 200 lines and it could be 50, rewrite it.
+
+Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
+
+## 3. Surgical Changes
+
+**Touch only what you must. Clean up only your own mess.**
+
+When editing existing code:
+- Don't "improve" adjacent code, comments, or formatting.
+- Don't refactor things that aren't broken.
+- Match existing style, even if you'd do it differently.
+- If you notice unrelated dead code, mention it - don't delete it.
+
+When your changes create orphans:
+- Remove imports/variables/functions that YOUR changes made unused.
+- Don't remove pre-existing dead code unless asked.
+
+The test: Every changed line should trace directly to the user's request.
+
+## 4. Goal-Driven Execution
+
+**Define success criteria. Loop until verified.**
+
+Transform tasks into verifiable goals:
+- "Add validation" → "Write tests for invalid inputs, then make them pass"
+- "Fix the bug" → "Write a test that reproduces it, then make it pass"
+- "Refactor X" → "Ensure tests pass before and after"
+
+For multi-step tasks, state a brief plan:
+```
+1. [Step] → verify: [check]
+2. [Step] → verify: [check]
+3. [Step] → verify: [check]
+```
+
+Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
diff --git a/lib/card-generator.ts b/lib/card-generator.ts
index 8356edb..9e34382 100644
--- a/lib/card-generator.ts
+++ b/lib/card-generator.ts
@@ -2,12 +2,8 @@ import type { CardType } from "./cards";
import { getCachedCard, setCachedCard } from "./cache";
import { generateStructuredResponse } from "./llm";
import {
- historicalAccuracySchema,
internationalRatingsSchema,
- productionFactsSchema,
ratingExplainedSchema,
- sensitivityGuideSchema,
- skipGuideSchema,
} from "./schemas";
import { formatMovieContext, getMovieDetails } from "./tmdb";
@@ -29,66 +25,14 @@ You MUST return ONLY valid JSON matching this exact structure (no extra keys, no
}
Focus on specific scenes or content patterns that drove the rating. Be precise with the allowed category and intensity values.`,
- skip_guide: `Create a skip guide for families who want to watch this film with a younger audience.
-You MUST return ONLY valid JSON matching this exact structure:
-{
- "originalRating": string,
- "suggestedAudienceAge": number (integer 0-21),
- "totalSkippedMinutes": number (integer),
- "segments": array of {
- "start": string (e.g. "0:05" or "12m"),
- "end": string,
- "reason": string,
- "intensity": one of exactly ["mild", "moderate", "strong"],
- "plotImpact": one of exactly ["none", "minor", "significant"],
- "skipRecommendation": one of exactly ["recommended", "optional", "do_not_skip"],
- "resumeHint": string
- },
- "disclaimer": string
-}
-Only include genuinely skippable segments. Mark plot-critical scenes as skipRecommendation "do_not_skip".
-Timestamps should be approximate for the standard theatrical cut. Use only the exact enum strings listed.`,
-
- sensitivity_guide: `Create a sensitivity guide grouping potentially upsetting content by theme.
-You MUST return ONLY valid JSON:
-{
- "summary": string,
- "themes": array of {
- "theme": string,
- "severity": one of exactly ["mild", "moderate", "strong"],
- "description": string,
- "approximateTiming": string (optional)
- }
-}
-Cover violence, sexuality, substances, self-harm, abuse, animal harm, discrimination, and other difficult themes where relevant. Use only the exact "severity" values.`,
-
international_ratings: `Summarize how this film is rated across countries using the certification data provided.
Return JSON with: ratings (array of {country, countryCode, rating, descriptors?}), discrepancyNote, strictestCountry (optional), mostLenientCountry (optional).
Include at least US, GB, ES, DE, FR when data exists. Explain notable differences.`,
-
- production_facts: `Curate interesting behind-the-scenes production facts for cinephiles.
-Return JSON with: facts (array of at least 5 {category, fact}), budget (optional string), boxOffice (optional string), filmingLocations (optional array of strings).
-Prefer specific, verifiable-sounding details; note uncertainty in the fact text when needed.`,
-
- historical_accuracy: `Evaluate historical accuracy for this biographical or historical film.
-You MUST return ONLY valid JSON:
-{
- "accuracyScore": number (0-100),
- "summary": string,
- "accurateElements": string[],
- "inventions": array of { "claim": string, "reality": string },
- "confidence": one of exactly ["high", "medium", "low"]
-}
-If the film is not historical/biographical, still assess any real-world basis it claims. Use only the exact confidence value.`,
};
const SCHEMAS = {
rating_explained: ratingExplainedSchema,
- skip_guide: skipGuideSchema,
- sensitivity_guide: sensitivityGuideSchema,
international_ratings: internationalRatingsSchema,
- production_facts: productionFactsSchema,
- historical_accuracy: historicalAccuracySchema,
} as const;
export async function generateCard(movieId: number, cardType: CardType) {
@@ -113,4 +57,4 @@ ${context}`;
await setCachedCard(movieId, cardType, data);
return { data, cached: false };
-}
\ No newline at end of file
+}
diff --git a/lib/cards.ts b/lib/cards.ts
index e165a10..4cbdaf1 100644
--- a/lib/cards.ts
+++ b/lib/cards.ts
@@ -1,10 +1,6 @@
export const CARD_TYPES = [
"rating_explained",
- "skip_guide",
- "sensitivity_guide",
"international_ratings",
- "production_facts",
- "historical_accuracy",
] as const;
export type CardType = (typeof CARD_TYPES)[number];
@@ -20,40 +16,12 @@ export const CARD_META: Record
= {
title: "Rating Explained",
subtitle: "Why this film has its age rating",
},
- skip_guide: {
- title: "Skip Guide",
- subtitle: "Scenes you can skip for a younger audience",
- badge: "Families",
- },
- sensitivity_guide: {
- title: "Sensitivity Guide",
- subtitle: "Content warnings by theme",
- },
international_ratings: {
title: "International Ratings",
subtitle: "How ratings differ by country",
},
- production_facts: {
- title: "Production Facts",
- subtitle: "Behind-the-scenes facts",
- },
- historical_accuracy: {
- title: "Historical Accuracy",
- subtitle: "Fact vs fiction breakdown",
- },
};
-const HISTORICAL_GENRES = new Set([
- "history",
- "war",
- "biography",
- "documentary",
-]);
-
-export function shouldShowHistoricalAccuracy(genres: string[]): boolean {
- return genres.some((genre) => HISTORICAL_GENRES.has(genre.toLowerCase()));
-}
-
export function isCardType(value: string): value is CardType {
return CARD_TYPES.includes(value as CardType);
-}
\ No newline at end of file
+}
diff --git a/lib/schemas/index.ts b/lib/schemas/index.ts
index fa69587..fe13b03 100644
--- a/lib/schemas/index.ts
+++ b/lib/schemas/index.ts
@@ -26,36 +26,6 @@ export const ratingExplainedSchema = z.object({
borderlineNote: z.string().optional(),
});
-export const skipGuideSchema = z.object({
- originalRating: z.string(),
- suggestedAudienceAge: z.number().int().min(0).max(21),
- totalSkippedMinutes: z.number().int().min(0),
- segments: z.array(
- z.object({
- start: z.string(),
- end: z.string(),
- reason: z.string(),
- intensity,
- plotImpact: z.enum(["none", "minor", "significant"]),
- skipRecommendation: z.enum(["recommended", "optional", "do_not_skip"]),
- resumeHint: z.string(),
- }),
- ),
- disclaimer: z.string(),
-});
-
-export const sensitivityGuideSchema = z.object({
- summary: z.string(),
- themes: z.array(
- z.object({
- theme: z.string(),
- severity: intensity,
- description: z.string(),
- approximateTiming: z.string().optional(),
- }),
- ),
-});
-
export const internationalRatingsSchema = z.object({
ratings: z.array(
z.object({
@@ -70,34 +40,5 @@ export const internationalRatingsSchema = z.object({
mostLenientCountry: z.string().optional(),
});
-export const productionFactsSchema = z.object({
- facts: z.array(
- z.object({
- category: z.string(),
- fact: z.string(),
- }),
- ),
- budget: z.string().optional(),
- boxOffice: z.string().optional(),
- filmingLocations: z.array(z.string()).optional(),
-});
-
-export const historicalAccuracySchema = z.object({
- accuracyScore: z.number().min(0).max(100),
- summary: z.string(),
- accurateElements: z.array(z.string()),
- inventions: z.array(
- z.object({
- claim: z.string(),
- reality: z.string(),
- }),
- ),
- confidence,
-});
-
export type RatingExplained = z.infer;
-export type SkipGuide = z.infer;
-export type SensitivityGuide = z.infer;
export type InternationalRatings = z.infer;
-export type ProductionFacts = z.infer;
-export type HistoricalAccuracy = z.infer;
\ No newline at end of file
diff --git a/lib/tmdb.ts b/lib/tmdb.ts
index 0f18f3b..89dacd2 100644
--- a/lib/tmdb.ts
+++ b/lib/tmdb.ts
@@ -36,10 +36,6 @@ export interface TmdbMovieDetails {
keywords?: {
keywords: Array<{ id: number; name: string }>;
};
- credits?: {
- cast: Array<{ name: string; character: string }>;
- crew: Array<{ name: string; job: string; department: string }>;
- };
}
function getApiKey(): string {
@@ -85,7 +81,7 @@ export async function searchMovies(query: string): Promise {
export async function getMovieDetails(movieId: number): Promise {
return tmdbFetch(`/movie/${movieId}`, {
- append_to_response: "release_dates,credits,keywords",
+ append_to_response: "release_dates,keywords",
});
}
@@ -115,14 +111,6 @@ export function formatMovieContext(movie: TmdbMovieDetails): string {
const genres = movie.genres.map((g) => g.name).join(", ");
const keywords = movie.keywords?.keywords.map((k) => k.name).join(", ") ?? "";
const certification = getUsCertification(movie);
- const directors = movie.credits?.crew
- .filter((c) => c.job === "Director")
- .map((c) => c.name)
- .join(", ");
- const leadCast = movie.credits?.cast
- .slice(0, 5)
- .map((c) => `${c.name} as ${c.character}`)
- .join("; ");
const releaseDates = movie.release_dates?.results
.map((country) => {
@@ -146,10 +134,6 @@ export function formatMovieContext(movie: TmdbMovieDetails): string {
`Overview: ${movie.overview}`,
certification ? `US Certification: ${certification}` : null,
keywords ? `Keywords: ${keywords}` : null,
- directors ? `Director(s): ${directors}` : null,
- leadCast ? `Lead cast: ${leadCast}` : null,
- movie.budget ? `Budget: $${movie.budget.toLocaleString("en-US")}` : null,
- movie.revenue ? `Revenue: $${movie.revenue.toLocaleString("en-US")}` : null,
releaseDates ? `International certifications:\n${releaseDates}` : null,
]
.filter(Boolean)
diff --git a/package-lock.json b/package-lock.json
index 27719b0..a326682 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2534,7 +2534,7 @@
"version": "20.19.42",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz",
"integrity": "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -8208,7 +8208,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {