import { useCallback, useEffect, useState } from "react"; import { getSuggestedVideos, listPredictions, predict, predictVideo, } from "../api/client"; import { CommentRow } from "../components/CommentRow"; import { SuggestedRail } from "../components/SuggestedRail"; import { useApp } from "../context/AppContext"; import { useDebouncedPredict } from "../hooks/useDebouncedPredict"; import { useI18n } from "../i18n/I18nContext"; import type { CommentItem, PredictionRecord, SuggestedVideo, } from "../types/api"; import { formatPct, newId, randomTeamMember, randomUsername, relativeTime, toxicityColor, truncate, } from "../utils/toxicity"; const DEFAULT_EMBED_VIDEO_ID = "A1uxPRUgimk"; function isPlaceholderTitle(title: string, id: string): boolean { return title === `Video ${id}`; } export function WatchPage() { const { t } = useI18n(); const { threshold, addHubEntry } = useApp(); const [draft, setDraft] = useState(""); const [sessionComments, setSessionComments] = useState([]); const [suggested, setSuggested] = useState([]); const [maxComments, setMaxComments] = useState(15); const [activeVideo, setActiveVideo] = useState(null); const [youtubeComments, setYoutubeComments] = useState([]); const [loadingVideoId, setLoadingVideoId] = useState(null); const [fetchError, setFetchError] = useState(null); const [demoBanner, setDemoBanner] = useState(false); const [dismissDemoBanner, setDismissDemoBanner] = useState(false); const [posting, setPosting] = useState(false); const [recentActivity, setRecentActivity] = useState([]); const [recentLoading, setRecentLoading] = useState(false); const { result, loading, error } = useDebouncedPredict(draft, threshold); const refreshRecent = useCallback(async (videoId?: string) => { if (!videoId) { setRecentActivity([]); return; } setRecentLoading(true); try { const res = await listPredictions(videoId, 200, "user_comment"); setRecentActivity(Array.isArray(res?.predictions) ? res.predictions : []); } catch { // Degrade gracefully if endpoint is missing or DB not configured setRecentActivity([]); } finally { setRecentLoading(false); } }, []); useEffect(() => { void refreshRecent(activeVideo?.id); }, [activeVideo?.id, refreshRecent]); useEffect(() => { getSuggestedVideos() .then((r) => { setSuggested(r.videos); setMaxComments(r.max_comments); // Auto-load first video so the user sees comments on initial render. if (r.videos.length > 0) { void loadVideo(r.videos[0]); } }) .catch(() => setFetchError(t.watch.couldNotLoadVideos)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handlePost = useCallback(async () => { const text = draft.trim(); if (!text || posting) return; setPosting(true); try { const author = randomTeamMember(); const analysis = await predict(text, threshold, { videoId: activeVideo?.id, author, persist: true, }); const item: CommentItem = { id: newId(), user: author, text, time: t.watch.justNow, is_toxic: analysis.is_toxic, probability: analysis.probability, labels: analysis.labels, source: "manual", }; setSessionComments((prev) => [...prev, item]); addHubEntry({ user: `@${author}`, snippet: text.slice(0, 45), score: analysis.probability, action: analysis.is_toxic ? `${t.watch.posted} (${t.badges.toxic.toLowerCase()})` : t.badges.safe, }); setDraft(""); void refreshRecent(activeVideo?.id); } finally { setPosting(false); } }, [draft, posting, threshold, addHubEntry, refreshRecent, activeVideo?.id, t]); const loadVideo = async (video: SuggestedVideo) => { setActiveVideo(video); setYoutubeComments([]); setSessionComments([]); setFetchError(null); setDismissDemoBanner(false); setLoadingVideoId(video.id); try { const res = await predictVideo(video.watch_url, maxComments, threshold); setDemoBanner(res.source === "demo"); setYoutubeComments( res.results.map((r, i) => ({ id: `yt-${video.id}-${i}`, user: randomUsername(`yt-${video.id}-${i}`), text: r.text, time: t.watch.fromYoutube, is_toxic: r.is_toxic, probability: r.probability, labels: r.labels, source: "youtube" as const, })) ); } catch (e) { setFetchError(e instanceof Error ? e.message : t.watch.failedToLoadComments); setYoutubeComments([]); setDemoBanner(false); } finally { setLoadingVideoId(null); } }; const toxicManual = sessionComments.filter((c) => c.is_toxic).length; const toxicYt = youtubeComments.filter((c) => c.is_toxic).length; const totalComments = sessionComments.length + youtubeComments.length; const channelInitial = activeVideo?.channel_title?.charAt(0).toUpperCase() ?? "Y"; return (
{activeVideo && !activeVideo.embeddable ? ( {t.watch.watchOnYoutube} ) : (