"use client"; import { TagInput, type Tag as TagType } from "emblor"; import { useAtom } from "jotai"; import { ArrowLeft, Info } from "lucide-react"; import { useTranslations } from "next-intl"; import { type FC, useCallback, useState } from "react"; import { toast } from "sonner"; import { createDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { baseApiService } from "@/lib/apis/base-api.service"; const YOUTUBE_VIDEO_URL_RE = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?[^\s]*v=[\w-]{11}|youtu\.be\/[\w-]{11})[^\s]*/; const YOUTUBE_PLAYLIST_URL_RE = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/[^\s]*[?&]list=[\w-]+[^\s]*/; const YOUTUBE_ANY_URL_RE = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch[^\s]*|playlist[^\s]*)|youtu\.be\/[\w-]+[^\s]*)/gi; function isYoutubeVideoUrl(url: string): boolean { return YOUTUBE_VIDEO_URL_RE.test(url.trim()); } function isYoutubePlaylistUrl(url: string): boolean { return YOUTUBE_PLAYLIST_URL_RE.test(url.trim()); } function extractYoutubeUrls(text: string): string[] { const matches = text.match(YOUTUBE_ANY_URL_RE); return matches ? [...new Set(matches)] : []; } interface YouTubeCrawlerViewProps { searchSpaceId: string; onBack: () => void; } export const YouTubeCrawlerView: FC = ({ searchSpaceId, onBack }) => { const t = useTranslations("add_youtube"); const [videoTags, setVideoTags] = useState([]); const [activeTagIndex, setActiveTagIndex] = useState(null); const [error, setError] = useState(null); const [isFetchingPlaylist, setIsFetchingPlaylist] = useState(false); const [createDocumentMutation] = useAtom(createDocumentMutationAtom); const { mutate: createYouTubeDocument, isPending: isSubmitting } = createDocumentMutation; const extractVideoId = (url: string): string | null => { const match = url.match(/(?:[?&]v=|youtu\.be\/)([\w-]{11})/); return match ? match[1] : null; }; const resolvePlaylist = useCallback( async (url: string) => { setIsFetchingPlaylist(true); toast(t("resolving_playlist_toast"), { description: t("resolving_playlist_toast_desc"), }); try { const response = (await baseApiService.get( `/api/v1/youtube/playlist-videos?url=${encodeURIComponent(url)}` )) as { video_urls: string[]; count: number }; const resolvedUrls: string[] = response.video_urls ?? []; setVideoTags((prev) => { const existingTexts = new Set(prev.map((tag) => tag.text)); const newTags = resolvedUrls .filter((vUrl) => !existingTexts.has(vUrl)) .map((vUrl) => ({ id: `${Date.now()}-${Math.random()}`, text: vUrl, })); return newTags.length > 0 ? [...prev, ...newTags] : prev; }); toast(t("playlist_resolved_toast"), { description: t("playlist_resolved_toast_desc", { count: resolvedUrls.length }), }); } catch (err) { const message = err instanceof Error ? err.message : t("error_generic"); toast(t("playlist_error_toast"), { description: message }); } finally { setIsFetchingPlaylist(false); } }, [t] ); const handlePaste = useCallback( async (e: React.ClipboardEvent) => { const text = e.clipboardData.getData("text/plain"); if (!text) return; const urls = extractYoutubeUrls(text); if (urls.length === 0) return; e.preventDefault(); const playlistUrls: string[] = []; const videoUrls: string[] = []; for (const url of urls) { if (isYoutubePlaylistUrl(url)) { playlistUrls.push(url); } else if (isYoutubeVideoUrl(url)) { videoUrls.push(url); } } if (videoUrls.length > 0) { setVideoTags((prev) => { const existingTexts = new Set(prev.map((tag) => tag.text)); const newTags = videoUrls .filter((url) => !existingTexts.has(url.trim())) .map((url) => ({ id: `${Date.now()}-${Math.random()}`, text: url.trim(), })); if (newTags.length === 0) { toast(t("duplicate_url_toast"), { description: t("duplicate_url_toast_desc"), }); } return newTags.length > 0 ? [...prev, ...newTags] : prev; }); } for (const url of playlistUrls) { await resolvePlaylist(url); } }, [resolvePlaylist, t] ); const handleSubmit = async () => { if (videoTags.length === 0) { setError(t("error_no_video")); return; } const invalidUrls = videoTags.filter((tag) => !isYoutubeVideoUrl(tag.text)); if (invalidUrls.length > 0) { setError(t("error_invalid_urls", { urls: invalidUrls.map((tag) => tag.text).join(", ") })); return; } setError(null); toast(t("processing_toast"), { description: t("processing_toast_desc"), }); const videoUrls = videoTags.map((tag) => tag.text); createYouTubeDocument( { document_type: "YOUTUBE_VIDEO", content: videoUrls, search_space_id: parseInt(searchSpaceId, 10), }, { onSuccess: () => { toast(t("success_toast"), { description: t("success_toast_desc"), }); onBack(); }, onError: (error: unknown) => { const errorMessage = error instanceof Error ? error.message : t("error_generic"); setError(errorMessage); toast(t("error_toast"), { description: `${t("error_toast_desc")}: ${errorMessage}`, }); }, } ); }; const handleAddTag = (text: string) => { if (isYoutubePlaylistUrl(text)) { resolvePlaylist(text); return; } if (!isYoutubeVideoUrl(text)) { toast(t("invalid_url_toast"), { description: t("invalid_url_toast_desc"), }); return; } if (videoTags.some((tag) => tag.text === text)) { toast(t("duplicate_url_toast"), { description: t("duplicate_url_toast_desc"), }); return; } const newTag: TagType = { id: Date.now().toString(), text: text, }; setVideoTags([...videoTags, newTag]); }; return (
{/* Header */}
{getConnectorIcon(EnumConnectorName.YOUTUBE_CONNECTOR, "h-7 w-7")}

{t("title")}

{t("subtitle")}

{/* Form Content - Scrollable */}
{/* Wrapper intercepts paste events for auto-detection of YouTube URLs */}

{t("hint")}

{isFetchingPlaylist && (
{t("resolving_playlist")}
)} {error &&
{error}
}

{t("chat_tip")}

{t("tips_title")}

  • {t("tip_1")}
  • {t("tip_2")}
  • {t("tip_3")}
  • {t("tip_4")}
  • {t("tip_5")}
{videoTags.length > 0 && videoTags.length <= 3 && (

{t("preview")}:

{videoTags.map((tag) => { const videoId = extractVideoId(tag.text); return videoId ? (