"use client"; import { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import AuthGuard from "@/components/auth-guard"; import NavBar from "@/components/nav-bar"; import { useApi } from "@/lib/use-api"; import ReactMarkdown from "react-markdown"; import SpeakableText from "@/components/speakable-text"; interface Category { ID: string; NAME: string; DEPTH: number; FULL_PATH: string; } interface KnowledgeItem { ID: string; TYPE: string; TITLE: string; SOURCE_URL: string; RAW_TEXT: string; STRUCTURED_CONTENT: string | null; STATUS: string; CREATED_AT: string; UPDATED_AT: string; CATEGORIES: Category[]; } interface Chunk { ID: string; CHUNK_INDEX: number; CONTENT: string; TOKEN_COUNT: number; } const statusColors: Record = { PENDING: "bg-yellow-500/20 text-yellow-400", EXTRACTING: "bg-blue-500/20 text-blue-400", CHUNKING: "bg-purple-500/20 text-purple-400", CATEGORIZING: "bg-indigo-500/20 text-indigo-400", STRUCTURING: "bg-orange-500/20 text-orange-400", EMBEDDING: "bg-cyan-500/20 text-cyan-400", READY: "bg-green-500/20 text-green-400", FAILED: "bg-red-500/20 text-red-400", }; const typeLabels: Record = { YOUTUBE: "YouTube", WEB: "Web", TEXT: "Text", }; 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 KnowledgeDetailPage() { const { request } = useApi(); const router = useRouter(); const params = useParams(); const id = params.id as string; const [item, setItem] = useState(null); const [chunks, setChunks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingTitle, setEditingTitle] = useState(false); const [titleDraft, setTitleDraft] = useState(""); const [showChunks, setShowChunks] = useState(false); const [deleting, setDeleting] = useState(false); const [generating, setGenerating] = useState(false); const [structuring, setStructuring] = useState(false); const [showStructured, setShowStructured] = useState(true); const fetchItem = async () => { try { const data = await request({ method: "GET", url: `/api/knowledge/${id}` }); setItem(data); } catch (err: unknown) { const msg = err instanceof Error ? err.message : "Failed to load"; setError(msg); } finally { setLoading(false); } }; const fetchChunks = async () => { try { const data = await request({ method: "GET", url: `/api/knowledge/${id}/chunks` }); setChunks(data); } catch (err) { console.error("Failed to load chunks:", err); } }; useEffect(() => { fetchItem(); }, [id]); // Poll while processing useEffect(() => { if (!item) return; const processing = ["PENDING", "EXTRACTING", "STRUCTURING", "CHUNKING", "CATEGORIZING", "EMBEDDING"].includes(item.STATUS); if (!processing) return; const interval = setInterval(fetchItem, 3000); return () => clearInterval(interval); }, [item?.STATUS]); const handleSaveTitle = async () => { if (!titleDraft.trim()) return; try { const updated = await request({ method: "PATCH", url: `/api/knowledge/${id}`, data: { title: titleDraft.trim() }, }); setItem(updated); setEditingTitle(false); } catch (err) { console.error("Failed to update title:", err); } }; const handleDelete = async () => { if (!confirm("정말 삭제하시겠습니까?")) return; setDeleting(true); try { await request({ method: "DELETE", url: `/api/knowledge/${id}` }); router.push("/knowledge"); } catch (err) { console.error("Failed to delete:", err); setDeleting(false); } }; const handleToggleChunks = () => { if (!showChunks && chunks.length === 0) { fetchChunks(); } setShowChunks(!showChunks); }; if (loading) { return (

Loading...

); } if (error || !item) { return (

{error || "Item not found"}

); } const videoId = item.TYPE === "YOUTUBE" && item.SOURCE_URL ? extractYouTubeVideoId(item.SOURCE_URL) : null; return (
{/* Back link */} {/* Header */}
{typeLabels[item.TYPE] || item.TYPE} {item.STATUS}
{/* Title (editable) */} {editingTitle ? (
setTitleDraft(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSaveTitle()} className="flex-1 px-3 py-1 rounded-lg bg-[var(--color-bg-hover)] border border-[var(--color-border)] focus:border-[var(--color-primary)] focus:outline-none text-lg font-bold" autoFocus />
) : (

{ setTitleDraft(item.TITLE || ""); setEditingTitle(true); }} title="클릭하여 제목 수정" > {item.TITLE || "Untitled"}

)} {/* Source URL */} {item.SOURCE_URL && (

{item.SOURCE_URL}

)} {/* Meta */}
생성: {new Date(item.CREATED_AT).toLocaleString("ko-KR")} 수정: {new Date(item.UPDATED_AT).toLocaleString("ko-KR")}
{/* YouTube Embed */} {videoId && (