initial commit
Some checks failed
Recommender Build and Deploy (internal) / Build Recommender Image (push) Failing after 3m48s
Recommender Build and Deploy (internal) / Deploy Recommender (internal) (push) Has been skipped

This commit is contained in:
2026-03-25 17:34:37 -03:00
commit f9c7582e4d
52 changed files with 7022 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
import { useState } from 'preact/hooks';
interface NewRecommendationModalProps {
onClose: () => void;
onSubmit: (body: {
main_prompt: string;
liked_shows: string;
disliked_shows: string;
themes: string;
}) => Promise<void>;
}
export function NewRecommendationModal({ onClose, onSubmit }: NewRecommendationModalProps) {
const [mainPrompt, setMainPrompt] = useState('');
const [likedShows, setLikedShows] = useState('');
const [dislikedShows, setDislikedShows] = useState('');
const [themes, setThemes] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!mainPrompt.trim()) return;
setLoading(true);
try {
await onSubmit({
main_prompt: mainPrompt.trim(),
liked_shows: likedShows.trim(),
disliked_shows: dislikedShows.trim(),
themes: themes.trim(),
});
onClose();
} finally {
setLoading(false);
}
};
const handleBackdropClick = (e: MouseEvent) => {
if ((e.target as HTMLElement).classList.contains('modal-backdrop')) {
onClose();
}
};
return (
<div class="modal-backdrop" onClick={handleBackdropClick}>
<div class="modal">
<div class="modal-header">
<h2>New Recommendation</h2>
<button class="modal-close" onClick={onClose} aria-label="Close">×</button>
</div>
<form class="modal-form" onSubmit={handleSubmit}>
<div class="form-group">
<label for="main-prompt">What are you looking for?</label>
<textarea
id="main-prompt"
class="form-textarea"
placeholder="Describe what you want to watch. Be as specific as you like — mood, themes, setting, what you've enjoyed before..."
value={mainPrompt}
onInput={(e) => setMainPrompt((e.target as HTMLTextAreaElement).value)}
rows={5}
required
/>
</div>
<div class="form-row">
<div class="form-group">
<label for="liked-shows">Shows you liked</label>
<input
id="liked-shows"
type="text"
class="form-input"
placeholder="e.g. Breaking Bad, The Wire"
value={likedShows}
onInput={(e) => setLikedShows((e.target as HTMLInputElement).value)}
/>
</div>
<div class="form-group">
<label for="disliked-shows">Shows you disliked</label>
<input
id="disliked-shows"
type="text"
class="form-input"
placeholder="e.g. Game of Thrones"
value={dislikedShows}
onInput={(e) => setDislikedShows((e.target as HTMLInputElement).value)}
/>
</div>
</div>
<div class="form-group">
<label for="themes">Themes and requirements</label>
<input
id="themes"
type="text"
class="form-input"
placeholder="e.g. dramatic setting, historical, sci-fi, etc."
value={themes}
onInput={(e) => setThemes((e.target as HTMLInputElement).value)}
/>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onClick={onClose} disabled={loading}>
Cancel
</button>
<button type="submit" class="btn-primary" disabled={loading || !mainPrompt.trim()}>
{loading ? 'Starting…' : 'Get Recommendations'}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import type { StageMap, StageStatus } from '../types/index.js';
const STAGES: { key: keyof StageMap; label: string }[] = [
{ key: 'interpreter', label: 'Interpreting Preferences' },
{ key: 'retrieval', label: 'Generating Candidates' },
{ key: 'ranking', label: 'Ranking Shows' },
{ key: 'curator', label: 'Curating Results' },
];
interface PipelineProgressProps {
stages: StageMap;
}
function StageIcon({ status }: { status: StageStatus }) {
switch (status) {
case 'done':
return <span class="stage-icon stage-done"></span>;
case 'error':
return <span class="stage-icon stage-error"></span>;
case 'running':
return <span class="stage-icon stage-running spinner"></span>;
default:
return <span class="stage-icon stage-pending"></span>;
}
}
export function PipelineProgress({ stages }: PipelineProgressProps) {
return (
<div class="pipeline-progress">
<h3 class="pipeline-title">Generating Recommendations</h3>
<ul class="pipeline-steps">
{STAGES.map(({ key, label }) => (
<li key={key} class={`pipeline-step pipeline-step--${stages[key]}`}>
<StageIcon status={stages[key]} />
<span class="pipeline-step-label">{label}</span>
</li>
))}
</ul>
</div>
);
}

View File

@@ -0,0 +1,85 @@
import { useState } from 'preact/hooks';
import type { CuratorOutput, CuratorCategory } from '../types/index.js';
interface RecommendationCardProps {
show: CuratorOutput;
existingFeedback?: { stars: number; feedback: string };
onFeedback: (tv_show_name: string, stars: number, feedback: string) => Promise<void>;
}
const CATEGORY_COLORS: Record<CuratorCategory, string> = {
'Definitely Like': 'badge-green',
'Might Like': 'badge-blue',
'Questionable': 'badge-yellow',
'Will Not Like': 'badge-red',
};
export function RecommendationCard({ show, existingFeedback, onFeedback }: RecommendationCardProps) {
const [selectedStars, setSelectedStars] = useState(existingFeedback?.stars ?? 0);
const [comment, setComment] = useState(existingFeedback?.feedback ?? '');
const [showComment, setShowComment] = useState(false);
const [submitted, setSubmitted] = useState(!!existingFeedback);
const [saving, setSaving] = useState(false);
const handleStarClick = (stars: number) => {
setSelectedStars(stars);
setShowComment(true);
};
const handleSubmit = async () => {
if (!selectedStars) return;
setSaving(true);
try {
await onFeedback(show.title, selectedStars, comment);
setSubmitted(true);
setShowComment(false);
} finally {
setSaving(false);
}
};
return (
<div class="card">
<div class="card-header">
<span class={`badge ${CATEGORY_COLORS[show.category]}`}>{show.category}</span>
<h3 class="card-title">{show.title}</h3>
</div>
<p class="card-explanation">{show.explanation}</p>
<div class="card-feedback">
<div class="star-rating">
{[1, 2, 3].map((star) => (
<button
key={star}
class={`star-btn${selectedStars >= star ? ' star-active' : ''}`}
onClick={() => handleStarClick(star)}
aria-label={`Rate ${star} star${star > 1 ? 's' : ''}`}
>
{selectedStars >= star ? '★' : '☆'}
</button>
))}
{submitted && <span class="feedback-saved">Saved</span>}
</div>
{showComment && !submitted && (
<div class="comment-area">
<textarea
class="form-textarea comment-input"
placeholder="Optional comment…"
value={comment}
onInput={(e) => setComment((e.target as HTMLTextAreaElement).value)}
rows={2}
/>
<button
class="btn-primary btn-sm"
onClick={handleSubmit}
disabled={saving || !selectedStars}
>
{saving ? 'Saving…' : 'Save Feedback'}
</button>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,60 @@
import type { RecommendationSummary } from '../types/index.js';
interface SidebarProps {
list: RecommendationSummary[];
selectedId: string | null;
onSelect: (id: string) => void;
onNewClick: () => void;
}
function statusIcon(status: RecommendationSummary['status']): string {
switch (status) {
case 'done': return '✓';
case 'error': return '✗';
case 'running': return '⟳';
default: return '…';
}
}
function statusClass(status: RecommendationSummary['status']): string {
switch (status) {
case 'done': return 'status-done';
case 'error': return 'status-error';
case 'running': return 'status-running';
default: return 'status-pending';
}
}
export function Sidebar({ list, selectedId, onSelect, onNewClick }: SidebarProps) {
return (
<aside class="sidebar">
<div class="sidebar-header">
<span class="sidebar-title">Recommender</span>
</div>
<button class="btn-new" onClick={onNewClick}>
+ New Recommendation
</button>
<div class="sidebar-section-label">History</div>
<ul class="sidebar-list">
{list.length === 0 && (
<li class="sidebar-empty">No recommendations yet</li>
)}
{list.map((item) => (
<li
key={item.id}
class={`sidebar-item${selectedId === item.id ? ' selected' : ''}`}
onClick={() => onSelect(item.id)}
>
<span class={`sidebar-icon ${statusClass(item.status)}`}>
{statusIcon(item.status)}
</span>
<span class="sidebar-item-title">{item.title}</span>
</li>
))}
</ul>
</aside>
);
}