"use client"; import { useState, useMemo, useEffect } from "react"; import { useRouter } from "next/navigation"; import AuthGuard from "@/components/auth-guard"; import NavBar from "@/components/nav-bar"; import { useApi } from "@/lib/use-api"; type KnowledgeType = "TEXT" | "WEB" | "YOUTUBE"; interface ModelInfo { id: string; name: string; vendor: string; } function extractYouTubeVideoId(url: string): string | null { try { const u = new URL(url); if (u.hostname === "youtu.be") return u.pathname.slice(1); if (u.hostname.includes("youtube.com")) return u.searchParams.get("v"); } catch { // invalid URL } return null; } export default function KnowledgeAddPage() { const { request } = useApi(); const router = useRouter(); const [type, setType] = useState("TEXT"); const [title, setTitle] = useState(""); const [url, setUrl] = useState(""); const [rawText, setRawText] = useState(""); const [modelId, setModelId] = useState(""); const [models, setModels] = useState([]); const [submitting, setSubmitting] = useState(false); const [fetchingTranscript, setFetchingTranscript] = useState(false); const [error, setError] = useState(null); useEffect(() => { request<{ models: ModelInfo[]; defaultModel: string; configured: boolean }>({ method: "GET", url: "/api/models", }).then((data) => { setModels(data.models); setModelId(data.defaultModel); }).catch((err) => { console.error("Failed to load models:", err); }); }, []); const videoId = useMemo(() => (type === "YOUTUBE" ? extractYouTubeVideoId(url) : null), [type, url]); const canFetchTranscript = type === "YOUTUBE" && videoId !== null && !fetchingTranscript; const handleFetchTranscript = async () => { if (!canFetchTranscript) return; setError(null); setFetchingTranscript(true); try { const data = await request<{ transcript?: string; error?: string }>({ method: "GET", url: `/api/knowledge/youtube-transcript?url=${encodeURIComponent(url.trim())}`, }); if (data.error) { setError(data.error); } else if (data.transcript) { setRawText(data.transcript); } } catch (err: unknown) { const msg = err instanceof Error ? err.message : "트랜스크립트를 가져올 수 없습니다"; setError(msg); } finally { setFetchingTranscript(false); } }; const canSubmit = !submitting && ((type === "TEXT" && rawText.trim().length > 0) || (type === "WEB" && url.trim().length > 0) || (type === "YOUTUBE" && url.trim().length > 0 && rawText.trim().length > 0)); const handleSubmit = async () => { setError(null); setSubmitting(true); try { await request({ method: "POST", url: "/api/knowledge/ingest", data: { type, title: title.trim() || null, url: type !== "TEXT" ? url.trim() : null, rawText: type !== "WEB" ? rawText.trim() : null, modelId: modelId || null, }, }); router.push("/knowledge"); } catch (err: unknown) { const msg = err instanceof Error ? err.message : "Failed to submit"; setError(msg); setSubmitting(false); } }; const types: { value: KnowledgeType; label: string }[] = [ { value: "TEXT", label: "Text" }, { value: "WEB", label: "Web" }, { value: "YOUTUBE", label: "YouTube" }, ]; // 벤더별 그룹화 const groupedModels = useMemo(() => { const groups: Record = {}; for (const m of models) { if (!groups[m.vendor]) groups[m.vendor] = []; groups[m.vendor].push(m); } return groups; }, [models]); return (

Add Knowledge

{/* Type Tabs */}
{types.map((t) => ( ))}
{/* Title */}
setTitle(e.target.value)} placeholder="비워두면 내용 기반으로 자동 생성" className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-card)] border border-[var(--color-border)] focus:border-[var(--color-primary)] focus:outline-none" />
{/* URL (WEB / YOUTUBE) */} {type !== "TEXT" && (
setUrl(e.target.value)} placeholder={type === "YOUTUBE" ? "https://www.youtube.com/watch?v=..." : "https://example.com/article"} className="w-full px-3 py-2 rounded-lg bg-[var(--color-bg-card)] border border-[var(--color-border)] focus:border-[var(--color-primary)] focus:outline-none" />
)} {/* YouTube Embed */} {type === "YOUTUBE" && videoId && (