"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui"; import { Button } from "@/components/ui"; import { Input } from "@/components/ui"; import { Modal, ModalContent, ModalHeader, ModalTitle } from "@/components/ui"; import { RAGEngine } from "@/rag"; interface KnowledgeEntry { id: string; title: string; content: string; category: string; tags: string[]; createdAt: string; updatedAt: string; status: "active" | "draft" | "archived"; description?: string; priority?: "high" | "medium" | "low"; } interface KnowledgeEditorProps { ragEngine: RAGEngine; onEntryCreated?: (entry: KnowledgeEntry) => void; onEntryUpdated?: (entry: KnowledgeEntry) => void; onEntryDeleted?: (entryId: string) => void; } export function KnowledgeEditor({ ragEngine, onEntryCreated, onEntryUpdated, onEntryDeleted, }: KnowledgeEditorProps) { const [entries, setEntries] = useState([]); const [filteredEntries, setFilteredEntries] = useState([]); const [editingEntry, setEditingEntry] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const [isBulkMode, setIsBulkMode] = useState(false); const [selectedEntries, setSelectedEntries] = useState>( new Set() ); // Form states const [formData, setFormData] = useState>({ title: "", content: "", category: "", tags: [], description: "", priority: "medium", status: "active", }); // Filter states const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [categoryFilter, setCategoryFilter] = useState(""); const [tagFilter, setTagFilter] = useState(""); // Auto-save states const [lastSaved, setLastSaved] = useState(null); const [isSaving, setIsSaving] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // Validation states const [validationErrors, setValidationErrors] = useState< Record >({}); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const autoSaveTimeoutRef = useRef(null); const textareaRef = useRef(null); // Categories and tags from existing entries const categories = [ ...new Set(entries.map((e) => e.category).filter(Boolean)), ]; const allTags = [...new Set(entries.flatMap((e) => e.tags).filter(Boolean))]; const loadEntries = useCallback(async () => { try { // This would load knowledge entries from your storage/database // For now, we'll use localStorage as a mock const stored = localStorage.getItem("knowledge-entries"); if (stored) { const loadedEntries = JSON.parse(stored); setEntries(loadedEntries); setFilteredEntries(loadedEntries); } } catch (err) { console.error("지식 항목 로드 실패:", err); setError("지식 항목을 불러오는데 실패했습니다."); } }, []); useEffect(() => { loadEntries(); }, [loadEntries]); // Filter entries useEffect(() => { let filtered = entries; if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filtered = filtered.filter( (entry) => entry.title.toLowerCase().includes(query) || entry.content.toLowerCase().includes(query) || entry.description?.toLowerCase().includes(query) || entry.tags.some((tag) => tag.toLowerCase().includes(query)) ); } if (statusFilter !== "all") { filtered = filtered.filter((entry) => entry.status === statusFilter); } if (categoryFilter) { filtered = filtered.filter((entry) => entry.category === categoryFilter); } if (tagFilter) { filtered = filtered.filter((entry) => entry.tags.includes(tagFilter)); } setFilteredEntries(filtered); }, [entries, searchQuery, statusFilter, categoryFilter, tagFilter]); const clearMessages = () => { setError(null); setSuccess(null); }; const validateForm = ( data: Partial ): Record => { const errors: Record = {}; if (!data.title?.trim()) { errors.title = "제목은 필수입니다."; } if (!data.content?.trim()) { errors.content = "내용은 필수입니다."; } if (data.content && data.content.trim().length < 10) { errors.content = "내용은 최소 10자 이상이어야 합니다."; } if (!data.category?.trim()) { errors.category = "카테고리는 필수입니다."; } return errors; }; const saveEntry = async ( entryData: Partial, isAutoSave = false ) => { try { if (!isAutoSave) { const errors = validateForm(entryData); if (Object.keys(errors).length > 0) { setValidationErrors(errors); return false; } } setIsSaving(true); clearMessages(); const now = new Date().toISOString(); const entry: KnowledgeEntry = { id: editingEntry?.id || `kb_${Date.now()}`, title: entryData.title || "", content: entryData.content || "", category: entryData.category || "", tags: entryData.tags || [], description: entryData.description || "", priority: entryData.priority || "medium", status: entryData.status || "active", createdAt: editingEntry?.createdAt || now, updatedAt: now, }; // Save to RAG system as a document if (entry.content.trim()) { const content = `# ${entry.title}\n\n${ entry.description ? `${entry.description}\n\n` : "" }${entry.content}`; const blob = new Blob([content], { type: "text/markdown" }); const file = new File([blob], `${entry.title}.md`, { type: "text/markdown", }); const metadata = { title: entry.title, category: entry.category, tags: entry.tags, type: "knowledge-entry", priority: entry.priority, status: entry.status, entryId: entry.id, }; if (editingEntry?.id) { // Update existing entry await ragEngine.uploadAndAddDocument( file, `${entry.title}.md`, metadata ); } else { // Add new entry await ragEngine.uploadAndAddDocument( file, `${entry.title}.md`, metadata ); } } // Update local storage const updatedEntries = editingEntry ? entries.map((e) => (e.id === editingEntry.id ? entry : e)) : [...entries, entry]; setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setLastSaved(new Date()); setHasUnsavedChanges(false); if (!isAutoSave) { setSuccess( editingEntry ? "지식 항목이 업데이트되었습니다." : "새 지식 항목이 생성되었습니다." ); if (editingEntry) { onEntryUpdated?.(entry); } else { onEntryCreated?.(entry); } setIsEditorOpen(false); resetForm(); } return true; } catch (err) { setError( `저장 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}` ); return false; } finally { setIsSaving(false); } }; const autoSave = useCallback(() => { if (hasUnsavedChanges && editingEntry) { saveEntry(formData, true); } }, [formData, hasUnsavedChanges, editingEntry, saveEntry]); // Auto-save with debounce useEffect(() => { if (hasUnsavedChanges && editingEntry) { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current); } autoSaveTimeoutRef.current = setTimeout(() => { autoSave(); }, 2000); // Auto-save after 2 seconds of inactivity } return () => { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current); } }; }, [hasUnsavedChanges, editingEntry, autoSave]); const resetForm = () => { setFormData({ title: "", content: "", category: "", tags: [], description: "", priority: "medium", status: "active", }); setEditingEntry(null); setValidationErrors({}); setHasUnsavedChanges(false); }; const openEditor = (entry?: KnowledgeEntry) => { if (entry) { setEditingEntry(entry); setFormData({ title: entry.title, content: entry.content, category: entry.category, tags: [...entry.tags], description: entry.description, priority: entry.priority, status: entry.status, }); } else { resetForm(); } setIsEditorOpen(true); setHasUnsavedChanges(false); }; const deleteEntry = async (entry: KnowledgeEntry) => { if (!confirm(`"${entry.title}" 항목을 삭제하시겠습니까?`)) return; try { const updatedEntries = entries.filter((e) => e.id !== entry.id); setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setSuccess(`"${entry.title}" 항목이 삭제되었습니다.`); onEntryDeleted?.(entry.id); } catch (err) { setError( `삭제 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}` ); } }; const bulkDelete = async () => { if (!confirm(`선택된 ${selectedEntries.size}개 항목을 삭제하시겠습니까?`)) return; try { const updatedEntries = entries.filter((e) => !selectedEntries.has(e.id)); setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setSuccess(`${selectedEntries.size}개 항목이 삭제되었습니다.`); setSelectedEntries(new Set()); setIsBulkMode(false); } catch (err) { setError( `벌크 삭제 실패: ${ err instanceof Error ? err.message : "알 수 없는 오류" }` ); } }; const bulkUpdateStatus = async (status: KnowledgeEntry["status"]) => { try { const updatedEntries = entries.map((e) => selectedEntries.has(e.id) ? { ...e, status, updatedAt: new Date().toISOString() } : e ); setEntries(updatedEntries); localStorage.setItem("knowledge-entries", JSON.stringify(updatedEntries)); setSuccess( `${selectedEntries.size}개 항목의 상태가 "${status}"로 변경되었습니다.` ); setSelectedEntries(new Set()); setIsBulkMode(false); } catch (err) { setError( `벌크 상태 변경 실패: ${ err instanceof Error ? err.message : "알 수 없는 오류" }` ); } }; const updateFormData = (field: keyof KnowledgeEntry, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); setHasUnsavedChanges(true); // Clear validation error for this field if (validationErrors[field]) { setValidationErrors((prev) => { const newErrors = { ...prev }; delete newErrors[field]; return newErrors; }); } }; const insertMarkdown = (markdown: string) => { if (textareaRef.current) { const textarea = textareaRef.current; const start = textarea.selectionStart; const end = textarea.selectionEnd; const currentContent = formData.content || ""; const newContent = currentContent.substring(0, start) + markdown + currentContent.substring(end); updateFormData("content", newContent); // Move cursor to end of inserted text setTimeout(() => { textarea.focus(); textarea.setSelectionRange( start + markdown.length, start + markdown.length ); }, 0); } }; const formatTime = (dateString: string) => { return new Date(dateString).toLocaleString(); }; return (
{/* Header with Controls */}
지식 베이스 편집기
{/* Search and Filters */}
setSearchQuery(e.target.value)} />
{/* Bulk Actions */} {isBulkMode && selectedEntries.size > 0 && (
{selectedEntries.size}개 항목 선택됨
)}
{/* Entries List */} 지식 항목 ({filteredEntries.length}개) {filteredEntries.length === 0 ? (

{searchQuery || statusFilter !== "all" || categoryFilter || tagFilter ? "검색 조건에 맞는 항목이 없습니다." : "지식 항목이 없습니다. 새 항목을 추가해보세요."}

) : (
{filteredEntries.map((entry) => (
{isBulkMode && ( { const newSelected = new Set(selectedEntries); if (e.target.checked) { newSelected.add(entry.id); } else { newSelected.delete(entry.id); } setSelectedEntries(newSelected); }} className="mt-1" /> )}

{entry.title}

{entry.description && (

{entry.description}

)}

카테고리: {entry.category}

{entry.tags.length > 0 && (

태그: {entry.tags.join(", ")}

)}

우선순위: {entry.priority}

상태: {entry.status}

마지막 수정: {formatTime(entry.updatedAt)}

{entry.content}

))}
)}
{/* Editor Modal */} {isEditorOpen && ( {editingEntry ? "지식 항목 편집" : "새 지식 항목"} {hasUnsavedChanges && ( ● 저장되지 않은 변경사항 )} {lastSaved && ( 마지막 저장: {lastSaved.toLocaleTimeString()} )}
{/* Basic Info */}
updateFormData("title", e.target.value)} placeholder="지식 항목 제목" className={validationErrors.title ? "border-red-500" : ""} /> {validationErrors.title && (

{validationErrors.title}

)}
updateFormData("category", e.target.value)} placeholder="예: FAQ, 기술문서, 가이드" list="categories" className={ validationErrors.category ? "border-red-500" : "" } /> {categories.map((cat) => ( {validationErrors.category && (

{validationErrors.category}

)}
updateFormData("description", e.target.value) } placeholder="간단한 설명" />
updateFormData( "tags", e.target.value .split(",") .map((tag) => tag.trim()) .filter(Boolean) ) } placeholder="예: 자주묻는질문, 기본설정" />
{/* Markdown Toolbar */}