- {!selectedId && ( -
-

TV Show Recommender

-

Click + New Recommendation to get started.

-
- )} - - {selectedId && isRunning && ( -
- -
- )} - - {selectedId && !isRunning && selectedRec?.status === 'done' && selectedRec.recommendations && ( -
-

{selectedRec.title}

- -
- {selectedRec.recommendations.map((show) => ( - { - await submitFeedback({ tv_show_name: name, stars, feedback: comment }); - }} - /> - ))} -
- -
- -
-
- )} - - {selectedId && !isRunning && selectedRec?.status === 'error' && ( -
-

Something went wrong

-

The pipeline encountered an error. You can try again by clicking Re-rank.

- -
- )} +
+

Recommender

+

Discover your next favorite show, powered by AI.

+
- {showModal && ( setShowModal(false)} diff --git a/packages/frontend/src/pages/Recom.tsx b/packages/frontend/src/pages/Recom.tsx new file mode 100644 index 0000000..1bda736 --- /dev/null +++ b/packages/frontend/src/pages/Recom.tsx @@ -0,0 +1,148 @@ +import { useState, useCallback, useEffect } from 'preact/hooks'; +import { route } from 'preact-router'; +import { PipelineProgress } from '../components/PipelineProgress.js'; +import { RecommendationCard } from '../components/RecommendationCard.js'; +import { useRecommendations } from '../hooks/useRecommendations.js'; +import { useSSE } from '../hooks/useSSE.js'; +import { getRecommendation } from '../api/client.js'; +import type { Recommendation, SSEEvent, StageMap, PipelineStage } from '../types/index.js'; + +interface RecomProps { + id: string; + path?: string; +} + +const DEFAULT_STAGES: StageMap = { + interpreter: 'pending', + retrieval: 'pending', + ranking: 'pending', + curator: 'pending', +}; + +const STAGE_ORDER: (keyof StageMap)[] = ['interpreter', 'retrieval', 'ranking', 'curator']; + +export function Recom({ id }: RecomProps) { + const { feedback, submitFeedback, rerank, updateStatus, refreshList } = useRecommendations(); + + const [rec, setRec] = useState(null); + const [stages, setStages] = useState(DEFAULT_STAGES); + const [sseUrl, setSseUrl] = useState(null); + + useEffect(() => { + setRec(null); + setStages(DEFAULT_STAGES); + setSseUrl(null); + getRecommendation(id) + .then((data) => { + setRec(data); + if (data.status === 'running' || data.status === 'pending') { + setStages(DEFAULT_STAGES); + setSseUrl(`/api/recommendations/${id}/stream`); + } + }) + .catch(() => route('/')); + }, [id]); + + const handleSSEEvent = useCallback( + (event: SSEEvent) => { + if (event.stage !== 'complete') { + const stageKey = event.stage as keyof StageMap; + if (STAGE_ORDER.includes(stageKey)) { + setStages((prev) => ({ + ...prev, + [stageKey]: event.status === 'start' ? 'running' : event.status === 'done' ? 'done' : 'error', + })); + } + } + + if (event.stage === 'complete' && event.status === 'done') { + setSseUrl(null); + updateStatus(id, 'done'); + void getRecommendation(id).then(setRec); + void refreshList(); + } + + if (event.status === 'error') { + setSseUrl(null); + updateStatus(id, 'error'); + const stageKey = event.stage as PipelineStage; + if (stageKey !== 'complete') { + setStages((prev) => ({ ...prev, [stageKey as keyof StageMap]: 'error' })); + } + } + }, + [id, updateStatus, refreshList], + ); + + useSSE(sseUrl, handleSSEEvent); + + const handleRerank = async () => { + await rerank(id); + setStages(DEFAULT_STAGES); + setSseUrl(`/api/recommendations/${id}/stream`); + setRec((prev) => (prev ? { ...prev, status: 'pending' } : null)); + }; + + const isRunning = rec?.status === 'running' || rec?.status === 'pending' || !!sseUrl; + const feedbackMap = new Map(feedback.map((f) => [f.tv_show_name, f])); + + return ( +
+ + +
+ {isRunning && ( +
+ +
+ )} + + {!isRunning && rec?.status === 'done' && rec.recommendations && ( +
+

{rec.title}

+
+ {rec.recommendations.map((show) => ( + { + await submitFeedback({ tv_show_name: name, stars, feedback: comment }); + }} + /> + ))} +
+
+ +
+
+ )} + + {!isRunning && rec?.status === 'error' && ( +
+

Something went wrong

+

The pipeline encountered an error. You can try again by clicking Re-rank.

+ +
+ )} +
+
+ ); +}