import { useEffect, useMemo, useRef, useState } from "react"; import { Languages } from "lucide-react"; import { toast } from "sonner"; import { useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import type { ItemDetail } from "@/api/types"; import { useMetadataAIStatus, useMetadataTranslationJobs, useTranslateItemMetadata, type MetadataTranslationJob, } from "@/hooks/queries/items"; import { invalidateMediaSurfaceQueries } from "@/hooks/queries/mediaSurfaceRefresh"; import { LANGUAGES } from "@/player/utils/languageNames"; function isActive(job: MetadataTranslationJob): boolean { return job.status === "pending" || job.status === "running"; } /** * "Translate with AI" block for the metadata editor: translates the item's * overview/tagline (and, for series, all season/episode overviews) into a * chosen language. Results land in the localization tables marked as * AI-sourced, so a later provider refresh can replace them but never the * other way around. Rendered only when the server has metadata AI enabled. */ export function MetadataTranslatePanel({ item }: { item: ItemDetail }) { const queryClient = useQueryClient(); const { data: status } = useMetadataAIStatus(); const enabled = Boolean(status?.enabled); const [targetLang, setTargetLang] = useState(""); const [force, setForce] = useState(false); const [watching, setWatching] = useState(false); const translateMutation = useTranslateItemMetadata(item.content_id); const { data: jobsData } = useMetadataTranslationJobs(item.content_id, enabled && watching); const activeJob = useMemo(() => (jobsData?.jobs ?? []).find(isActive), [jobsData]); const lastJob = jobsData?.jobs?.[0]; // When the job we were watching reaches a terminal state, surface the // outcome and refresh the (now possibly re-localized) detail surfaces. const sawActiveRef = useRef(false); useEffect(() => { if (activeJob) { sawActiveRef.current = true; return; } if (!sawActiveRef.current || !lastJob) return; sawActiveRef.current = false; setWatching(false); if (lastJob.status === "completed") { toast.success( lastJob.fields_total === 0 ? "Nothing to translate — all descriptions are already localized." : `Translated ${lastJob.fields_done} description${lastJob.fields_done === 1 ? "" : "s"}.`, ); void invalidateMediaSurfaceQueries(queryClient, { itemId: item.content_id }); } else if (lastJob.status === "failed") { toast.error(lastJob.error_message || "Translation failed."); } }, [activeJob, lastJob, queryClient, item.content_id]); if (!enabled) return null; const busy = translateMutation.isPending || Boolean(activeJob); const translatesChildren = item.type === "series"; const description = item.type === "series" ? "Translates the overview and tagline plus all season and episode overviews" : item.type === "movie" ? "Translates the overview and tagline" : "Translates the overview"; function start() { if (!targetLang) { toast.error("Pick a target language first."); return; } translateMutation.mutate( { target_language: targetLang, include_children: translatesChildren, force }, { onSuccess: () => setWatching(true) }, ); } return (
Translate with AI

{description} into the chosen language. Translations are served to libraries using that metadata language; provider data replaces them when it becomes available.

{activeJob && (

{activeJob.progress_message || "Working"}…{" "} {activeJob.fields_total > 0 && `${activeJob.fields_done}/${activeJob.fields_total} fields`}

)}
); }