mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-20 21:18:13 +02:00
feat: add YouTube video and playlist support in document collection with enhanced URL handling
This commit is contained in:
parent
e481415655
commit
c6fc4edbc2
10 changed files with 445 additions and 100 deletions
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import { TagInput, type Tag as TagType } from "emblor";
|
||||
import { useAtom } from "jotai";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { ArrowLeft, Info } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type FC, useState } from "react";
|
||||
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";
|
||||
|
|
@ -12,9 +12,29 @@ 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 youtubeRegex =
|
||||
/^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/;
|
||||
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;
|
||||
|
|
@ -26,27 +46,107 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
const [videoTags, setVideoTags] = useState<TagType[]>([]);
|
||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isFetchingPlaylist, setIsFetchingPlaylist] = useState(false);
|
||||
|
||||
// Use the createDocumentMutationAtom
|
||||
const [createDocumentMutation] = useAtom(createDocumentMutationAtom);
|
||||
const { mutate: createYouTubeDocument, isPending: isSubmitting } = createDocumentMutation;
|
||||
|
||||
const isValidYoutubeUrl = (url: string): boolean => {
|
||||
return youtubeRegex.test(url);
|
||||
};
|
||||
|
||||
const extractVideoId = (url: string): string | null => {
|
||||
const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
|
||||
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<HTMLDivElement>) => {
|
||||
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) => !isValidYoutubeUrl(tag.text));
|
||||
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;
|
||||
|
|
@ -60,7 +160,6 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
|
||||
const videoUrls = videoTags.map((tag) => tag.text);
|
||||
|
||||
// Use the mutation to create YouTube documents
|
||||
createYouTubeDocument(
|
||||
{
|
||||
document_type: "YOUTUBE_VIDEO",
|
||||
|
|
@ -86,7 +185,12 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
};
|
||||
|
||||
const handleAddTag = (text: string) => {
|
||||
if (!isValidYoutubeUrl(text)) {
|
||||
if (isYoutubePlaylistUrl(text)) {
|
||||
resolvePlaylist(text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isYoutubeVideoUrl(text)) {
|
||||
toast(t("invalid_url_toast"), {
|
||||
description: t("invalid_url_toast_desc"),
|
||||
});
|
||||
|
|
@ -111,7 +215,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10">
|
||||
<div className="shrink-0 px-6 sm:px-12 pt-8 sm:pt-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
|
|
@ -139,31 +243,48 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
<Label htmlFor="video-input" className="text-sm sm:text-base">
|
||||
{t("label")}
|
||||
</Label>
|
||||
<TagInput
|
||||
id="video-input"
|
||||
tags={videoTags}
|
||||
setTags={setVideoTags}
|
||||
placeholder={t("placeholder")}
|
||||
onAddTag={handleAddTag}
|
||||
styleClasses={{
|
||||
inlineTagsContainer:
|
||||
"border border-slate-400/20 rounded-lg bg-muted/50 shadow-sm shadow-black/5 transition-shadow focus-within:border-slate-400/40 focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 p-1 gap-1",
|
||||
input:
|
||||
"w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent",
|
||||
tag: {
|
||||
body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex",
|
||||
closeButton:
|
||||
"absolute -inset-y-px -end-px p-0 rounded-e-lg flex size-7 transition-colors outline-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 text-muted-foreground/80 hover:text-foreground",
|
||||
},
|
||||
}}
|
||||
activeTagIndex={activeTagIndex}
|
||||
setActiveTagIndex={setActiveTagIndex}
|
||||
/>
|
||||
{/* Wrapper intercepts paste events for auto-detection of YouTube URLs */}
|
||||
<div onPasteCapture={handlePaste}>
|
||||
<TagInput
|
||||
id="video-input"
|
||||
tags={videoTags}
|
||||
setTags={setVideoTags}
|
||||
placeholder={t("placeholder")}
|
||||
onAddTag={handleAddTag}
|
||||
styleClasses={{
|
||||
inlineTagsContainer:
|
||||
"border border-slate-400/20 rounded-lg bg-muted/50 shadow-sm shadow-black/5 transition-shadow focus-within:border-slate-400/40 focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 p-1 gap-1",
|
||||
input:
|
||||
"w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent",
|
||||
tag: {
|
||||
body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex",
|
||||
closeButton:
|
||||
"absolute -inset-y-px -end-px p-0 rounded-e-lg flex size-7 transition-colors outline-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 text-muted-foreground/80 hover:text-foreground",
|
||||
},
|
||||
}}
|
||||
activeTagIndex={activeTagIndex}
|
||||
setActiveTagIndex={setActiveTagIndex}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t("hint")}</p>
|
||||
</div>
|
||||
|
||||
{isFetchingPlaylist && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Spinner size="sm" />
|
||||
<span>{t("resolving_playlist")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="text-sm text-red-500 mt-2">{error}</div>}
|
||||
|
||||
<div className="flex items-start gap-3 rounded-lg border border-blue-200/50 bg-blue-50/50 dark:border-blue-500/20 dark:bg-blue-950/20 p-4 text-sm">
|
||||
<Info className="size-4 mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<p className="text-muted-foreground">
|
||||
{t("chat_tip")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-4 text-sm">
|
||||
<h4 className="font-medium mb-2">{t("tips_title")}</h4>
|
||||
<ul className="list-disc pl-5 space-y-1 text-muted-foreground">
|
||||
|
|
@ -171,14 +292,15 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
<li>{t("tip_2")}</li>
|
||||
<li>{t("tip_3")}</li>
|
||||
<li>{t("tip_4")}</li>
|
||||
<li>{t("tip_5")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{videoTags.length > 0 && (
|
||||
{videoTags.length > 0 && videoTags.length <= 3 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<h4 className="font-medium">{t("preview")}:</h4>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{videoTags.map((tag, _index) => {
|
||||
{videoTags.map((tag) => {
|
||||
const videoId = extractVideoId(tag.text);
|
||||
return videoId ? (
|
||||
<div
|
||||
|
|
@ -203,18 +325,18 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
</div>
|
||||
|
||||
{/* Fixed Footer - Action buttons */}
|
||||
<div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted border-t border-border">
|
||||
<div className="shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted border-t border-border">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onBack}
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || isFetchingPlaylist}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || videoTags.length === 0}
|
||||
disabled={isSubmitting || isFetchingPlaylist || videoTags.length === 0}
|
||||
className="text-xs sm:text-sm min-w-[140px] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"use client";
|
||||
import { useFeatureFlagVariantKey } from "@posthog/react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
|
|
@ -47,8 +46,6 @@ function useIsDesktop(breakpoint = 1024) {
|
|||
export function HeroSection() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const heroVariant = useFeatureFlagVariantKey("notebooklm_superpowers_flag");
|
||||
const isNotebookLMVariant = heroVariant === "superpowers";
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
return (
|
||||
|
|
@ -99,19 +96,11 @@ export function HeroSection() {
|
|||
)}
|
||||
|
||||
<h2 className="relative z-50 mx-auto mb-4 mt-8 max-w-4xl text-balance text-center text-3xl font-semibold tracking-tight text-gray-700 md:text-7xl dark:text-neutral-300">
|
||||
{isNotebookLMVariant ? (
|
||||
<div className="relative mx-auto inline-block w-max filter-[drop-shadow(0px_1px_3px_rgba(27,37,80,0.14))]">
|
||||
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
||||
<Balancer>NotebookLM with Superpowers</Balancer>
|
||||
</div>
|
||||
<div className="relative mx-auto inline-block w-max filter-[drop-shadow(0px_1px_3px_rgba(27,37,80,0.14))]">
|
||||
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
||||
<Balancer>NotebookLM for Teams</Balancer>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative mx-auto inline-block w-max filter-[drop-shadow(0px_1px_3px_rgba(27,37,80,0.14))]">
|
||||
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
||||
<Balancer>NotebookLM for Teams</Balancer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</h2>
|
||||
<p className="relative z-50 mx-auto mt-4 max-w-lg px-6 text-center text-sm leading-relaxed text-gray-600 sm:text-base sm:leading-relaxed md:max-w-xl md:text-lg md:leading-relaxed dark:text-gray-200">
|
||||
Connect any LLM to your internal knowledge sources and chat with it in real time alongside
|
||||
|
|
@ -187,20 +176,6 @@ function GetStartedButton() {
|
|||
);
|
||||
}
|
||||
|
||||
function ContactSalesButton() {
|
||||
return (
|
||||
<motion.div whileHover={{ scale: 1.02, y: -2 }} whileTap={{ scale: 0.98 }}>
|
||||
<Link
|
||||
href="/contact"
|
||||
//target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group relative z-20 flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-white px-6 py-2.5 text-sm font-semibold text-neutral-700 shadow-lg ring-1 ring-neutral-200/50 transition-shadow duration-300 hover:shadow-xl sm:w-56 dark:bg-neutral-900 dark:text-neutral-200 dark:ring-neutral-700/50"
|
||||
>
|
||||
Contact Sales
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const BackgroundGrids = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@ export const createDocumentRequest = document
|
|||
});
|
||||
|
||||
export const createDocumentResponse = z.object({
|
||||
message: z.literal("Documents created successfully"),
|
||||
message: z.literal("Documents queued for background processing"),
|
||||
status: z.literal("queued"),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -433,15 +433,17 @@
|
|||
},
|
||||
"add_youtube": {
|
||||
"title": "Add YouTube Videos",
|
||||
"subtitle": "Enter YouTube video URLs to add to your document collection",
|
||||
"label": "Enter YouTube Video URLs",
|
||||
"placeholder": "Enter a YouTube URL and press Enter",
|
||||
"hint": "Add multiple YouTube URLs by pressing Enter after each one",
|
||||
"subtitle": "Add YouTube videos or playlists to your knowledge base",
|
||||
"label": "YouTube Video or Playlist URLs",
|
||||
"placeholder": "Paste YouTube video or playlist URLs here",
|
||||
"hint": "Just paste YouTube URLs and they're added automatically. You can also type a URL and press Enter.",
|
||||
"tips_title": "Tips for adding YouTube videos:",
|
||||
"tip_1": "Use standard YouTube URLs (youtube.com/watch?v= or youtu.be/)",
|
||||
"tip_2": "Make sure videos are publicly accessible",
|
||||
"tip_3": "Supported formats: youtube.com/watch?v=VIDEO_ID or youtu.be/VIDEO_ID",
|
||||
"tip_4": "Processing may take some time depending on video length",
|
||||
"tip_5": "Paste a playlist URL (youtube.com/playlist?list=...) to add all its videos at once",
|
||||
"chat_tip": "Want a quick summary without saving to your knowledge base? Just paste YouTube links directly into the chat instead.",
|
||||
"preview": "Preview",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Add",
|
||||
|
|
@ -456,9 +458,15 @@
|
|||
"error_toast_desc": "Error processing YouTube videos",
|
||||
"error_generic": "An error occurred while processing YouTube videos",
|
||||
"invalid_url_toast": "Invalid YouTube URL",
|
||||
"invalid_url_toast_desc": "Please enter a valid YouTube video URL",
|
||||
"invalid_url_toast_desc": "Please enter a valid YouTube video or playlist URL",
|
||||
"duplicate_url_toast": "Duplicate URL",
|
||||
"duplicate_url_toast_desc": "This YouTube video has already been added"
|
||||
"duplicate_url_toast_desc": "This YouTube video has already been added",
|
||||
"resolving_playlist": "Resolving playlist videos...",
|
||||
"resolving_playlist_toast": "Resolving Playlist",
|
||||
"resolving_playlist_toast_desc": "Fetching video list from the playlist...",
|
||||
"playlist_resolved_toast": "Playlist Resolved",
|
||||
"playlist_resolved_toast_desc": "Added {count} videos from the playlist",
|
||||
"playlist_error_toast": "Playlist Error"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
|
|
|
|||
|
|
@ -433,15 +433,17 @@
|
|||
},
|
||||
"add_youtube": {
|
||||
"title": "Agregar videos de YouTube",
|
||||
"subtitle": "Ingresa URLs de videos de YouTube para agregar a tu colección de documentos",
|
||||
"label": "Ingresa URLs de videos de YouTube",
|
||||
"placeholder": "Ingresa una URL de YouTube y presiona Enter",
|
||||
"hint": "Agrega múltiples URLs de YouTube presionando Enter después de cada una",
|
||||
"subtitle": "Agrega videos o listas de reproducción de YouTube a tu base de conocimiento",
|
||||
"label": "URLs de videos o listas de reproducción de YouTube",
|
||||
"placeholder": "Pega URLs de videos o listas de reproducción de YouTube aquí",
|
||||
"hint": "Solo pega URLs de YouTube y se agregan automáticamente. También puedes escribir una URL y presionar Enter.",
|
||||
"tips_title": "Consejos para agregar videos de YouTube:",
|
||||
"tip_1": "Usa URLs estándar de YouTube (youtube.com/watch?v= o youtu.be/)",
|
||||
"tip_2": "Asegúrate de que los videos sean accesibles públicamente",
|
||||
"tip_3": "Formatos soportados: youtube.com/watch?v=VIDEO_ID o youtu.be/VIDEO_ID",
|
||||
"tip_4": "El procesamiento puede tomar un tiempo dependiendo de la duración del video",
|
||||
"tip_5": "Pega una URL de lista de reproducción (youtube.com/playlist?list=...) para agregar todos sus videos a la vez",
|
||||
"chat_tip": "¿Quieres un resumen rápido sin guardarlo en tu base de conocimiento? Solo pega los enlaces de YouTube directamente en el chat.",
|
||||
"preview": "Vista previa",
|
||||
"cancel": "Cancelar",
|
||||
"submit": "Agregar",
|
||||
|
|
@ -456,9 +458,15 @@
|
|||
"error_toast_desc": "Error al procesar videos de YouTube",
|
||||
"error_generic": "Ocurrió un error al procesar videos de YouTube",
|
||||
"invalid_url_toast": "URL de YouTube inválida",
|
||||
"invalid_url_toast_desc": "Por favor ingresa una URL válida de video de YouTube",
|
||||
"invalid_url_toast_desc": "Por favor ingresa una URL válida de video o lista de reproducción de YouTube",
|
||||
"duplicate_url_toast": "URL duplicada",
|
||||
"duplicate_url_toast_desc": "Este video de YouTube ya ha sido agregado"
|
||||
"duplicate_url_toast_desc": "Este video de YouTube ya ha sido agregado",
|
||||
"resolving_playlist": "Cargando videos de la lista de reproducción...",
|
||||
"resolving_playlist_toast": "Cargando lista de reproducción",
|
||||
"resolving_playlist_toast_desc": "Obteniendo la lista de videos de la lista de reproducción...",
|
||||
"playlist_resolved_toast": "Lista de reproducción cargada",
|
||||
"playlist_resolved_toast_desc": "Se agregaron {count} videos de la lista de reproducción",
|
||||
"playlist_error_toast": "Error de lista de reproducción"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
|
|
|
|||
|
|
@ -433,15 +433,17 @@
|
|||
},
|
||||
"add_youtube": {
|
||||
"title": "YouTube वीडियो जोड़ें",
|
||||
"subtitle": "अपने दस्तावेज़ संग्रह में जोड़ने के लिए YouTube वीडियो URL दर्ज करें",
|
||||
"label": "YouTube वीडियो URL दर्ज करें",
|
||||
"placeholder": "YouTube URL दर्ज करें और Enter दबाएं",
|
||||
"hint": "प्रत्येक के बाद Enter दबाकर कई YouTube URL जोड़ें",
|
||||
"subtitle": "अपने ज्ञान आधार में YouTube वीडियो या प्लेलिस्ट जोड़ें",
|
||||
"label": "YouTube वीडियो या प्लेलिस्ट URL",
|
||||
"placeholder": "YouTube वीडियो या प्लेलिस्ट URL यहां पेस्ट करें",
|
||||
"hint": "बस YouTube URL पेस्ट करें और वे स्वचालित रूप से जुड़ जाएंगे। आप URL टाइप करके Enter भी दबा सकते हैं।",
|
||||
"tips_title": "YouTube वीडियो जोड़ने के लिए सुझाव:",
|
||||
"tip_1": "मानक YouTube URL का उपयोग करें (youtube.com/watch?v= या youtu.be/)",
|
||||
"tip_2": "सुनिश्चित करें कि वीडियो सार्वजनिक रूप से सुलभ हैं",
|
||||
"tip_3": "समर्थित प्रारूप: youtube.com/watch?v=VIDEO_ID या youtu.be/VIDEO_ID",
|
||||
"tip_4": "वीडियो की अवधि के आधार पर प्रोसेसिंग में कुछ समय लग सकता है",
|
||||
"tip_5": "सभी वीडियो एक साथ जोड़ने के लिए प्लेलिस्ट URL (youtube.com/playlist?list=...) पेस्ट करें",
|
||||
"chat_tip": "ज्ञान आधार में सहेजे बिना त्वरित सारांश चाहिए? इसके बजाय YouTube लिंक सीधे चैट में पेस्ट करें।",
|
||||
"preview": "पूर्वावलोकन",
|
||||
"cancel": "रद्द करें",
|
||||
"submit": "जोड़ें",
|
||||
|
|
@ -456,9 +458,15 @@
|
|||
"error_toast_desc": "YouTube वीडियो प्रोसेस करने में त्रुटि",
|
||||
"error_generic": "YouTube वीडियो प्रोसेस करते समय त्रुटि हुई",
|
||||
"invalid_url_toast": "अमान्य YouTube URL",
|
||||
"invalid_url_toast_desc": "कृपया एक मान्य YouTube वीडियो URL दर्ज करें",
|
||||
"invalid_url_toast_desc": "कृपया एक मान्य YouTube वीडियो या प्लेलिस्ट URL दर्ज करें",
|
||||
"duplicate_url_toast": "डुप्लिकेट URL",
|
||||
"duplicate_url_toast_desc": "यह YouTube वीडियो पहले से जोड़ा जा चुका है"
|
||||
"duplicate_url_toast_desc": "यह YouTube वीडियो पहले से जोड़ा जा चुका है",
|
||||
"resolving_playlist": "प्लेलिस्ट वीडियो लोड हो रहे हैं...",
|
||||
"resolving_playlist_toast": "प्लेलिस्ट लोड हो रही है",
|
||||
"resolving_playlist_toast_desc": "प्लेलिस्ट से वीडियो सूची प्राप्त हो रही है...",
|
||||
"playlist_resolved_toast": "प्लेलिस्ट लोड हो गई",
|
||||
"playlist_resolved_toast_desc": "प्लेलिस्ट से {count} वीडियो जोड़े गए",
|
||||
"playlist_error_toast": "प्लेलिस्ट त्रुटि"
|
||||
},
|
||||
"settings": {
|
||||
"title": "सेटिंग्स",
|
||||
|
|
|
|||
|
|
@ -433,15 +433,17 @@
|
|||
},
|
||||
"add_youtube": {
|
||||
"title": "Adicionar vídeos do YouTube",
|
||||
"subtitle": "Insira URLs de vídeos do YouTube para adicionar à sua coleção de documentos",
|
||||
"label": "Insira URLs de vídeos do YouTube",
|
||||
"placeholder": "Insira uma URL do YouTube e pressione Enter",
|
||||
"hint": "Adicione múltiplas URLs do YouTube pressionando Enter após cada uma",
|
||||
"subtitle": "Adicione vídeos ou playlists do YouTube à sua base de conhecimento",
|
||||
"label": "URLs de vídeos ou playlists do YouTube",
|
||||
"placeholder": "Cole URLs de vídeos ou playlists do YouTube aqui",
|
||||
"hint": "Basta colar URLs do YouTube e elas são adicionadas automaticamente. Você também pode digitar uma URL e pressionar Enter.",
|
||||
"tips_title": "Dicas para adicionar vídeos do YouTube:",
|
||||
"tip_1": "Use URLs padrão do YouTube (youtube.com/watch?v= ou youtu.be/)",
|
||||
"tip_2": "Certifique-se de que os vídeos sejam acessíveis publicamente",
|
||||
"tip_3": "Formatos suportados: youtube.com/watch?v=VIDEO_ID ou youtu.be/VIDEO_ID",
|
||||
"tip_4": "O processamento pode levar algum tempo dependendo da duração do vídeo",
|
||||
"tip_5": "Cole uma URL de playlist (youtube.com/playlist?list=...) para adicionar todos os vídeos de uma vez",
|
||||
"chat_tip": "Quer um resumo rápido sem salvar na sua base de conhecimento? Basta colar os links do YouTube diretamente no chat.",
|
||||
"preview": "Visualizar",
|
||||
"cancel": "Cancelar",
|
||||
"submit": "Adicionar",
|
||||
|
|
@ -456,9 +458,15 @@
|
|||
"error_toast_desc": "Erro ao processar vídeos do YouTube",
|
||||
"error_generic": "Ocorreu um erro ao processar vídeos do YouTube",
|
||||
"invalid_url_toast": "URL do YouTube inválida",
|
||||
"invalid_url_toast_desc": "Por favor, insira uma URL válida de vídeo do YouTube",
|
||||
"invalid_url_toast_desc": "Por favor, insira uma URL válida de vídeo ou playlist do YouTube",
|
||||
"duplicate_url_toast": "URL duplicada",
|
||||
"duplicate_url_toast_desc": "Este vídeo do YouTube já foi adicionado"
|
||||
"duplicate_url_toast_desc": "Este vídeo do YouTube já foi adicionado",
|
||||
"resolving_playlist": "Carregando vídeos da playlist...",
|
||||
"resolving_playlist_toast": "Carregando playlist",
|
||||
"resolving_playlist_toast_desc": "Obtendo a lista de vídeos da playlist...",
|
||||
"playlist_resolved_toast": "Playlist carregada",
|
||||
"playlist_resolved_toast_desc": "Adicionados {count} vídeos da playlist",
|
||||
"playlist_error_toast": "Erro na playlist"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
|
|
|
|||
|
|
@ -417,18 +417,20 @@
|
|||
},
|
||||
"add_youtube": {
|
||||
"title": "添加 YouTube 视频",
|
||||
"subtitle": "输入 YouTube 视频 URL 以添加到您的文档集合",
|
||||
"label": "输入 YouTube 视频 URL",
|
||||
"placeholder": "输入 YouTube URL 并按 Enter",
|
||||
"hint": "按 Enter 键添加多个 YouTube URL",
|
||||
"subtitle": "将 YouTube 视频或播放列表添加到您的知识库",
|
||||
"label": "YouTube 视频或播放列表 URL",
|
||||
"placeholder": "在此粘贴 YouTube 视频或播放列表 URL",
|
||||
"hint": "直接粘贴 YouTube URL 即可自动添加。您也可以输入 URL 后按 Enter。",
|
||||
"tips_title": "添加 YouTube 视频的提示:",
|
||||
"tip_1": "使用标准 YouTube URL(youtube.com/watch?v= 或 youtu.be/)",
|
||||
"tip_2": "确保视频可公开访问",
|
||||
"tip_3": "支持的格式:youtube.com/watch?v=VIDEO_ID 或 youtu.be/VIDEO_ID",
|
||||
"tip_4": "处理时间可能会根据视频长度而有所不同",
|
||||
"tip_5": "粘贴播放列表 URL(youtube.com/playlist?list=...)可一次添加所有视频",
|
||||
"chat_tip": "想要快速摘要而不保存到知识库?直接在聊天中粘贴 YouTube 链接即可。",
|
||||
"preview": "预览",
|
||||
"cancel": "取消",
|
||||
"submit": "提交 YouTube 视频",
|
||||
"submit": "添加",
|
||||
"processing": "处理中...",
|
||||
"error_no_video": "请至少添加一个 YouTube 视频 URL",
|
||||
"error_invalid_urls": "检测到无效的 YouTube URL:{urls}",
|
||||
|
|
@ -440,9 +442,15 @@
|
|||
"error_toast_desc": "处理 YouTube 视频时出错",
|
||||
"error_generic": "处理 YouTube 视频时发生错误",
|
||||
"invalid_url_toast": "无效的 YouTube URL",
|
||||
"invalid_url_toast_desc": "请输入有效的 YouTube 视频 URL",
|
||||
"invalid_url_toast_desc": "请输入有效的 YouTube 视频或播放列表 URL",
|
||||
"duplicate_url_toast": "重复的 URL",
|
||||
"duplicate_url_toast_desc": "此 YouTube 视频已添加"
|
||||
"duplicate_url_toast_desc": "此 YouTube 视频已添加",
|
||||
"resolving_playlist": "正在加载播放列表视频...",
|
||||
"resolving_playlist_toast": "正在加载播放列表",
|
||||
"resolving_playlist_toast_desc": "正在从播放列表获取视频列表...",
|
||||
"playlist_resolved_toast": "播放列表已加载",
|
||||
"playlist_resolved_toast_desc": "已从播放列表添加 {count} 个视频",
|
||||
"playlist_error_toast": "播放列表错误"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue