changes and improvements
This commit is contained in:
@@ -7,7 +7,10 @@ const CuratorSchema = z.object({
|
|||||||
shows: z.array(z.object({
|
shows: z.array(z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
explanation: z.string(),
|
explanation: z.string(),
|
||||||
category: z.enum(["Definitely Like", "Might Like", "Questionable", "Will Not Like"])
|
category: z.enum(["Full Match", "Definitely Like", "Might Like", "Questionable", "Will Not Like"]),
|
||||||
|
genre: z.string(),
|
||||||
|
pros: z.array(z.string()).max(3),
|
||||||
|
cons: z.array(z.string()).max(3)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ export async function runCurator(
|
|||||||
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
|
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
|
||||||
|
|
||||||
const allShows = [
|
const allShows = [
|
||||||
|
...(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.definitely_like.map((t) => ({ title: t, category: 'Definitely Like' as const })),
|
||||||
...ranking.might_like.map((t) => ({ title: t, category: 'Might Like' as const })),
|
...ranking.might_like.map((t) => ({ title: t, category: 'Might Like' as const })),
|
||||||
...ranking.questionable.map((t) => ({ title: t, category: 'Questionable' as const })),
|
...ranking.questionable.map((t) => ({ title: t, category: 'Questionable' as const })),
|
||||||
@@ -39,19 +43,22 @@ export async function runCurator(
|
|||||||
...serviceOptions,
|
...serviceOptions,
|
||||||
...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
|
...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
|
||||||
text: { format: zodTextFormat(CuratorSchema, "shows") },
|
text: { format: zodTextFormat(CuratorSchema, "shows") },
|
||||||
instructions: `You are a ${mediaLabel} recommendation curator. For each ${mediaLabel}, write a concise 1-2 sentence explanation of why it was assigned to its category based on the user's preferences.${useWebSearch ? '\n\nUse web search to verify details and enrich explanations with accurate information.' : ''}
|
instructions: `You are a ${mediaLabel} recommendation curator. For each ${mediaLabel}, write a concise explanation and surface the most useful details for the user.${useWebSearch ? '\n\nUse web search to verify details and enrich explanations with accurate information.' : ''}
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Preserve the exact title and category as given
|
- Preserve the exact title and category as given
|
||||||
- Keep explanations concise (1-2 sentences max)
|
- explanation: 1-2 sentences explaining why it was assigned to its category, referencing specific user preferences
|
||||||
- Reference specific user preferences in the explanation
|
- genre: 1-3 words capturing the most prominent genre of the title (e.g. "Crime Drama", "Sci-Fi Thriller", "Romantic Comedy")
|
||||||
|
- pros: up to 3 short bullet points about what this title does well relative to the user's taste
|
||||||
|
- 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`,
|
- Be honest — explain why "Questionable" or "Will Not Like" ${mediaLabel}s got that rating`,
|
||||||
input: `User preferences summary:
|
input: `User preferences summary:
|
||||||
Liked: ${JSON.stringify(interpreter.liked)}
|
Liked: ${interpreter.liked.join(', ') || '(none)'}
|
||||||
Themes: ${JSON.stringify(interpreter.themes)}
|
Themes: ${interpreter.themes.join(', ') || '(none)'}
|
||||||
Tone: ${JSON.stringify(interpreter.tone)}
|
Tone: ${interpreter.tone.join(', ') || '(none)'}
|
||||||
Character preferences: ${JSON.stringify(interpreter.character_preferences)}
|
Character preferences: ${interpreter.character_preferences.join(', ') || '(none)'}
|
||||||
Avoid: ${JSON.stringify(interpreter.avoid)}
|
Avoid: ${interpreter.avoid.join(', ') || '(none)'}
|
||||||
|
Requirements: ${interpreter.requirements.join(', ') || '(none)'}
|
||||||
|
|
||||||
${mediaLabel}s to describe:
|
${mediaLabel}s to describe:
|
||||||
${showList}`,
|
${showList}`,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const InterpreterSchema = z.object({
|
|||||||
themes: z.array(z.string()),
|
themes: z.array(z.string()),
|
||||||
character_preferences: z.array(z.string()),
|
character_preferences: z.array(z.string()),
|
||||||
tone: z.array(z.string()),
|
tone: z.array(z.string()),
|
||||||
avoid: z.array(z.string())
|
avoid: z.array(z.string()),
|
||||||
|
requirements: z.array(z.string())
|
||||||
});
|
});
|
||||||
|
|
||||||
interface InterpreterInput {
|
interface InterpreterInput {
|
||||||
@@ -39,7 +40,8 @@ Rules:
|
|||||||
- Normalize terminology (e.g. "spy" → "espionage", "cop show" → "police procedural")
|
- Normalize terminology (e.g. "spy" → "espionage", "cop show" → "police procedural")
|
||||||
- Detect and resolve contradictions (prefer explicit over implicit)
|
- Detect and resolve contradictions (prefer explicit over implicit)
|
||||||
- Do NOT assume anything not stated or clearly implied
|
- Do NOT assume anything not stated or clearly implied
|
||||||
- Be specific and concrete, not vague`,
|
- 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}
|
input: `Main prompt: ${input.main_prompt}
|
||||||
Liked ${mediaLabel}s: ${input.liked_shows || '(none)'}
|
Liked ${mediaLabel}s: ${input.liked_shows || '(none)'}
|
||||||
Disliked ${mediaLabel}s: ${input.disliked_shows || '(none)'}
|
Disliked ${mediaLabel}s: ${input.disliked_shows || '(none)'}
|
||||||
@@ -47,6 +49,6 @@ Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`,
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (response.output_parsed as InterpreterOutput) ?? {
|
return (response.output_parsed as InterpreterOutput) ?? {
|
||||||
liked: [], disliked: [], themes: [], character_preferences: [], tone: [], avoid: []
|
liked: [], disliked: [], themes: [], character_preferences: [], tone: [], avoid: [], requirements: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
|||||||
import { zodTextFormat } from 'openai/helpers/zod';
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
|
|
||||||
const RankingSchema = z.object({
|
const RankingSchema = z.object({
|
||||||
|
full_match: z.array(z.string()),
|
||||||
definitely_like: z.array(z.string()),
|
definitely_like: z.array(z.string()),
|
||||||
might_like: z.array(z.string()),
|
might_like: z.array(z.string()),
|
||||||
questionable: z.array(z.string()),
|
questionable: z.array(z.string()),
|
||||||
@@ -32,6 +33,7 @@ export async function runRanking(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allTags: RankingOutput = {
|
const allTags: RankingOutput = {
|
||||||
|
full_match: [],
|
||||||
definitely_like: [],
|
definitely_like: [],
|
||||||
might_like: [],
|
might_like: [],
|
||||||
questionable: [],
|
questionable: [],
|
||||||
@@ -46,21 +48,23 @@ export async function runRanking(
|
|||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
...serviceOptions,
|
...serviceOptions,
|
||||||
text: { format: zodTextFormat(RankingSchema, "ranking") },
|
text: { format: zodTextFormat(RankingSchema, "ranking") },
|
||||||
instructions: `You are a ${mediaLabel} ranking critic. Assign each ${mediaLabel} to exactly one of four confidence tags based on how well it matches the user's preferences.
|
instructions: `You are a ${mediaLabel} ranking critic. Assign each ${mediaLabel} to exactly one of five confidence tags based on how well it matches the user's preferences.
|
||||||
|
|
||||||
Tags:
|
Tags:
|
||||||
- "definitely_like": Near-perfect match to all preferences
|
- "full_match": 100% match — perfectly satisfies every stated preference, requirement, and avoidance criteria with no compromises
|
||||||
|
- "definitely_like": Near-perfect match to all preferences with only minor caveats
|
||||||
- "might_like": Strong match to most preferences
|
- "might_like": Strong match to most preferences
|
||||||
- "questionable": Partial alignment, some aspects don't match
|
- "questionable": Partial alignment, some aspects don't match
|
||||||
- "will_not_like": Likely mismatch, conflicts with preferences or avoidance criteria
|
- "will_not_like": Likely mismatch, conflicts with preferences or avoidance criteria
|
||||||
|
|
||||||
Every ${mediaLabel} in the input must appear in exactly one tag. Use the title exactly as given.`,
|
Every ${mediaLabel} in the input must appear in exactly one tag. Use the title exactly as given.`,
|
||||||
input: `User preferences:
|
input: `User preferences:
|
||||||
Liked ${mediaLabel}s: ${JSON.stringify(interpreter.liked)}
|
Liked ${mediaLabel}s: ${interpreter.liked.join(', ') || '(none)'}
|
||||||
Themes: ${JSON.stringify(interpreter.themes)}
|
Themes: ${interpreter.themes.join(', ') || '(none)'}
|
||||||
Character preferences: ${JSON.stringify(interpreter.character_preferences)}
|
Character preferences: ${interpreter.character_preferences.join(', ') || '(none)'}
|
||||||
Tone: ${JSON.stringify(interpreter.tone)}
|
Tone: ${interpreter.tone.join(', ') || '(none)'}
|
||||||
Avoid: ${JSON.stringify(interpreter.avoid)}
|
Avoid: ${interpreter.avoid.join(', ') || '(none)'}
|
||||||
|
Requirements: ${interpreter.requirements.join(', ') || '(none)'}
|
||||||
|
|
||||||
Rank these ${mediaLabel}s:
|
Rank these ${mediaLabel}s:
|
||||||
${chunkTitles}`,
|
${chunkTitles}`,
|
||||||
@@ -68,6 +72,7 @@ ${chunkTitles}`,
|
|||||||
|
|
||||||
const chunkResult = (response.output_parsed as Partial<RankingOutput>) ?? {};
|
const chunkResult = (response.output_parsed as Partial<RankingOutput>) ?? {};
|
||||||
|
|
||||||
|
allTags.full_match.push(...(chunkResult.full_match ?? []));
|
||||||
allTags.definitely_like.push(...(chunkResult.definitely_like ?? []));
|
allTags.definitely_like.push(...(chunkResult.definitely_like ?? []));
|
||||||
allTags.might_like.push(...(chunkResult.might_like ?? []));
|
allTags.might_like.push(...(chunkResult.might_like ?? []));
|
||||||
allTags.questionable.push(...(chunkResult.questionable ?? []));
|
allTags.questionable.push(...(chunkResult.questionable ?? []));
|
||||||
|
|||||||
@@ -36,12 +36,13 @@ Rules:
|
|||||||
- Include ${mediaLabelPlural} from different decades, countries${mediaType === 'tv_show' ? ', and networks' : ', and directors'}
|
- Include ${mediaLabelPlural} from different decades, countries${mediaType === 'tv_show' ? ', and networks' : ', and directors'}
|
||||||
- Aim for ${brainstormCount} candidates minimum`,
|
- Aim for ${brainstormCount} candidates minimum`,
|
||||||
input: `Structured preferences:
|
input: `Structured preferences:
|
||||||
Liked ${mediaLabelPlural}: ${JSON.stringify(input.liked)}
|
Liked ${mediaLabelPlural}: ${input.liked.join(', ') || '(none)'}
|
||||||
Disliked ${mediaLabelPlural}: ${JSON.stringify(input.disliked)}
|
Disliked ${mediaLabelPlural}: ${input.disliked.join(', ') || '(none)'}
|
||||||
Themes: ${JSON.stringify(input.themes)}
|
Themes: ${input.themes.join(', ') || '(none)'}
|
||||||
Character preferences: ${JSON.stringify(input.character_preferences)}
|
Character preferences: ${input.character_preferences.join(', ') || '(none)'}
|
||||||
Tone: ${JSON.stringify(input.tone)}
|
Tone: ${input.tone.join(', ') || '(none)'}
|
||||||
Avoid: ${JSON.stringify(input.avoid)}
|
Avoid: ${input.avoid.join(', ') || '(none)'}
|
||||||
|
Requirements: ${input.requirements.join(', ') || '(none)'}
|
||||||
|
|
||||||
Generate a large, diverse pool of ${mediaLabel} candidates.`,
|
Generate a large, diverse pool of ${mediaLabel} candidates.`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -124,12 +124,14 @@ export async function runPipeline(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
const rankingOutput: RankingOutput = {
|
const rankingOutput: RankingOutput = {
|
||||||
|
full_match: rankingBuckets.flatMap((r) => r.full_match),
|
||||||
definitely_like: rankingBuckets.flatMap((r) => r.definitely_like),
|
definitely_like: rankingBuckets.flatMap((r) => r.definitely_like),
|
||||||
might_like: rankingBuckets.flatMap((r) => r.might_like),
|
might_like: rankingBuckets.flatMap((r) => r.might_like),
|
||||||
questionable: rankingBuckets.flatMap((r) => r.questionable),
|
questionable: rankingBuckets.flatMap((r) => r.questionable),
|
||||||
will_not_like: rankingBuckets.flatMap((r) => r.will_not_like),
|
will_not_like: rankingBuckets.flatMap((r) => r.will_not_like),
|
||||||
};
|
};
|
||||||
log(rec.id, `Ranking: done (${Date.now() - t2}ms) — ${rankBucketCount} buckets`, {
|
log(rec.id, `Ranking: done (${Date.now() - t2}ms) — ${rankBucketCount} buckets`, {
|
||||||
|
full_match: rankingOutput.full_match.length,
|
||||||
definitely_like: rankingOutput.definitely_like.length,
|
definitely_like: rankingOutput.definitely_like.length,
|
||||||
might_like: rankingOutput.might_like.length,
|
might_like: rankingOutput.might_like.length,
|
||||||
questionable: rankingOutput.questionable.length,
|
questionable: rankingOutput.questionable.length,
|
||||||
@@ -144,6 +146,7 @@ export async function runPipeline(
|
|||||||
const t3 = Date.now();
|
const t3 = Date.now();
|
||||||
type CategorizedItem = { title: string; category: keyof RankingOutput };
|
type CategorizedItem = { title: string; category: keyof RankingOutput };
|
||||||
const categorizedItems: CategorizedItem[] = [
|
const categorizedItems: CategorizedItem[] = [
|
||||||
|
...rankingOutput.full_match.map((t) => ({ title: t, category: 'full_match' as const })),
|
||||||
...rankingOutput.definitely_like.map((t) => ({ title: t, category: 'definitely_like' as const })),
|
...rankingOutput.definitely_like.map((t) => ({ title: t, category: 'definitely_like' as const })),
|
||||||
...rankingOutput.might_like.map((t) => ({ title: t, category: 'might_like' as const })),
|
...rankingOutput.might_like.map((t) => ({ title: t, category: 'might_like' as const })),
|
||||||
...rankingOutput.questionable.map((t) => ({ title: t, category: 'questionable' as const })),
|
...rankingOutput.questionable.map((t) => ({ title: t, category: 'questionable' as const })),
|
||||||
@@ -152,6 +155,7 @@ export async function runPipeline(
|
|||||||
const curatorBucketCount = getBucketCount(categorizedItems.length);
|
const curatorBucketCount = getBucketCount(categorizedItems.length);
|
||||||
const curatorItemBuckets = splitIntoBuckets(categorizedItems, curatorBucketCount);
|
const curatorItemBuckets = splitIntoBuckets(categorizedItems, curatorBucketCount);
|
||||||
const curatorBucketRankings: RankingOutput[] = curatorItemBuckets.map((bucket) => ({
|
const curatorBucketRankings: RankingOutput[] = curatorItemBuckets.map((bucket) => ({
|
||||||
|
full_match: bucket.filter((i) => i.category === 'full_match').map((i) => i.title),
|
||||||
definitely_like: bucket.filter((i) => i.category === 'definitely_like').map((i) => i.title),
|
definitely_like: bucket.filter((i) => i.category === 'definitely_like').map((i) => i.title),
|
||||||
might_like: bucket.filter((i) => i.category === 'might_like').map((i) => i.title),
|
might_like: bucket.filter((i) => i.category === 'might_like').map((i) => i.title),
|
||||||
questionable: bucket.filter((i) => i.category === 'questionable').map((i) => i.title),
|
questionable: bucket.filter((i) => i.category === 'questionable').map((i) => i.title),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface InterpreterOutput {
|
|||||||
character_preferences: string[];
|
character_preferences: string[];
|
||||||
tone: string[];
|
tone: string[];
|
||||||
avoid: string[];
|
avoid: string[];
|
||||||
|
requirements: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RetrievalCandidate {
|
export interface RetrievalCandidate {
|
||||||
@@ -19,18 +20,22 @@ export interface RetrievalOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RankingOutput {
|
export interface RankingOutput {
|
||||||
|
full_match: string[];
|
||||||
definitely_like: string[];
|
definitely_like: string[];
|
||||||
might_like: string[];
|
might_like: string[];
|
||||||
questionable: string[];
|
questionable: string[];
|
||||||
will_not_like: string[];
|
will_not_like: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CuratorCategory = 'Definitely Like' | 'Might Like' | 'Questionable' | 'Will Not Like';
|
export type CuratorCategory = 'Full Match' | 'Definitely Like' | 'Might Like' | 'Questionable' | 'Will Not Like';
|
||||||
|
|
||||||
export interface CuratorOutput {
|
export interface CuratorOutput {
|
||||||
title: string;
|
title: string;
|
||||||
explanation: string;
|
explanation: string;
|
||||||
category: CuratorCategory;
|
category: CuratorCategory;
|
||||||
|
genre: string;
|
||||||
|
pros: string[];
|
||||||
|
cons: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PipelineStage = 'interpreter' | 'retrieval' | 'ranking' | 'curator' | 'complete';
|
export type PipelineStage = 'interpreter' | 'retrieval' | 'ranking' | 'curator' | 'complete';
|
||||||
|
|||||||
@@ -57,6 +57,11 @@
|
|||||||
color: #f87171;
|
color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-magenta {
|
||||||
|
background: rgba(217, 70, 239, 0.15);
|
||||||
|
color: #e879f9;
|
||||||
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -67,7 +72,42 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(148, 163, 184, 0.12);
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pros-cons-table {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pros-col,
|
||||||
|
.cons-col {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pro-item {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.con-item {
|
||||||
|
color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-feedback {
|
.card-feedback {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface RecommendationCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_COLORS: Record<CuratorCategory, string> = {
|
const CATEGORY_COLORS: Record<CuratorCategory, string> = {
|
||||||
|
'Full Match': 'badge-magenta',
|
||||||
'Definitely Like': 'badge-green',
|
'Definitely Like': 'badge-green',
|
||||||
'Might Like': 'badge-blue',
|
'Might Like': 'badge-blue',
|
||||||
'Questionable': 'badge-yellow',
|
'Questionable': 'badge-yellow',
|
||||||
@@ -48,6 +49,19 @@ export function RecommendationCard({ show, existingFeedback, onFeedback }: Recom
|
|||||||
</div>
|
</div>
|
||||||
<p class="card-explanation">{show.explanation}</p>
|
<p class="card-explanation">{show.explanation}</p>
|
||||||
|
|
||||||
|
{show.genre && <span class="badge genre-badge">{show.genre}</span>}
|
||||||
|
|
||||||
|
{(show.pros?.length > 0 || show.cons?.length > 0) && (
|
||||||
|
<div class="pros-cons-table">
|
||||||
|
<div class="pros-col">
|
||||||
|
{show.pros?.map((p) => <span class="pro-item">+ {p}</span>)}
|
||||||
|
</div>
|
||||||
|
<div class="cons-col">
|
||||||
|
{show.cons?.map((c) => <span class="con-item">- {c}</span>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div class="card-feedback">
|
<div class="card-feedback">
|
||||||
<div class="star-rating">
|
<div class="star-rating">
|
||||||
{[1, 2, 3].map((star) => {
|
{[1, 2, 3].map((star) => {
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ export function Recom({ id }: RecomProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div class="rec-info-row rec-info-delete-row">
|
<div class="rec-info-row rec-info-delete-row">
|
||||||
|
{!isRunning && (
|
||||||
|
<button class="btn-rerun btn-rerank" onClick={handleRetry}>
|
||||||
|
Re-run Pipeline
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
class="btn-danger"
|
class="btn-danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -223,9 +228,6 @@ export function Recom({ id }: RecomProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="rerank-section">
|
<div class="rerank-section">
|
||||||
<button class="btn-rerank btn-rerun" onClick={handleRetry}>
|
|
||||||
Re-run Pipeline
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="btn-rerank"
|
class="btn-rerank"
|
||||||
onClick={handleRerank}
|
onClick={handleRerank}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
export type MediaType = 'tv_show' | 'movie';
|
export type MediaType = 'tv_show' | 'movie';
|
||||||
|
|
||||||
export type CuratorCategory = 'Definitely Like' | 'Might Like' | 'Questionable' | 'Will Not Like';
|
export type CuratorCategory = 'Full Match' | 'Definitely Like' | 'Might Like' | 'Questionable' | 'Will Not Like';
|
||||||
|
|
||||||
export interface CuratorOutput {
|
export interface CuratorOutput {
|
||||||
title: string;
|
title: string;
|
||||||
explanation: string;
|
explanation: string;
|
||||||
category: CuratorCategory;
|
category: CuratorCategory;
|
||||||
|
genre: string;
|
||||||
|
pros: string[];
|
||||||
|
cons: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RecommendationStatus = 'pending' | 'running' | 'done' | 'error';
|
export type RecommendationStatus = 'pending' | 'running' | 'done' | 'error';
|
||||||
|
|||||||
Reference in New Issue
Block a user