"use client"; import { useState, useEffect, useCallback } 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 QualityMetrics { totalDocuments: number; totalChunks: number; avgChunkSize: number; embeddingDimensions: number; vectorStoreSize: number; lastUpdated: string; } interface RetrievalTest { id: string; query: string; expectedAnswers: string[]; results: { content: string; score: number; metadata: Record; }[]; accuracy: number; timestamp: string; notes?: string; } interface KnowledgeGap { id: string; category: string; description: string; priority: "high" | "medium" | "low"; suggestedActions: string[]; relatedQueries: string[]; } interface RAGManagerProps { ragEngine: RAGEngine; onRetrainTriggered?: () => void; } export function RAGManager({ ragEngine, onRetrainTriggered }: RAGManagerProps) { const [metrics, setMetrics] = useState(null); const [retrievalTests, setRetrievalTests] = useState([]); const [knowledgeGaps, setKnowledgeGaps] = useState([]); const [isTestModalOpen, setIsTestModalOpen] = useState(false); const [isGapModalOpen, setIsGapModalOpen] = useState(false); const [isRetesting, setIsRetesting] = useState(false); const [isAnalyzing, setIsAnalyzing] = useState(false); // Test form states const [testQuery, setTestQuery] = useState(""); const [expectedAnswers, setExpectedAnswers] = useState([""]); const [testNotes, setTestNotes] = useState(""); // Gap form states const [gapCategory, setGapCategory] = useState(""); const [gapDescription, setGapDescription] = useState(""); const [gapPriority, setGapPriority] = useState<"high" | "medium" | "low">( "medium" ); const [gapActions, setGapActions] = useState([""]); const [gapQueries, setGapQueries] = useState([""]); // Analytics data const [frequentQueries, setFrequentQueries] = useState< Array<{ query: string; count: number; avgScore: number; lastUsed: string; }> >([]); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const loadMetrics = useCallback(async () => { try { const stats = await ragEngine.getStats(); const searchStats = ragEngine.getSearchStats(); setMetrics({ totalDocuments: stats.documentCount, totalChunks: stats.chunkCount, avgChunkSize: stats.chunkCount > 0 ? 1000 : 0, // Placeholder embeddingDimensions: 1536, // OpenAI default vectorStoreSize: stats.chunkCount * 1536 * 4, // Approximate bytes lastUpdated: new Date().toISOString(), }); } catch (err) { console.error("메트릭 로드 실패:", err); setError("메트릭을 불러오는데 실패했습니다."); } }, [ragEngine]); const loadTestResults = useCallback(() => { try { const stored = localStorage.getItem("rag-retrieval-tests"); if (stored) { setRetrievalTests(JSON.parse(stored)); } } catch (err) { console.error("테스트 결과 로드 실패:", err); } }, []); const loadKnowledgeGaps = useCallback(() => { try { const stored = localStorage.getItem("rag-knowledge-gaps"); if (stored) { setKnowledgeGaps(JSON.parse(stored)); } } catch (err) { console.error("지식 격차 로드 실패:", err); } }, []); const loadAnalytics = useCallback(() => { try { const stored = localStorage.getItem("rag-query-analytics"); if (stored) { setFrequentQueries(JSON.parse(stored)); } else { // Mock data for demonstration setFrequentQueries([ { query: "AI 모델 훈련 방법", count: 45, avgScore: 0.89, lastUsed: new Date(Date.now() - 86400000).toISOString(), }, { query: "RAG 시스템 구현", count: 32, avgScore: 0.92, lastUsed: new Date(Date.now() - 3600000).toISOString(), }, { query: "벡터 데이터베이스 최적화", count: 28, avgScore: 0.85, lastUsed: new Date(Date.now() - 7200000).toISOString(), }, ]); } } catch (err) { console.error("분석 데이터 로드 실패:", err); } }, []); useEffect(() => { loadMetrics(); loadTestResults(); loadKnowledgeGaps(); loadAnalytics(); }, [loadMetrics, loadTestResults, loadKnowledgeGaps, loadAnalytics]); const clearMessages = () => { setError(null); setSuccess(null); }; const runRetrievalTest = async () => { if (!testQuery.trim()) { setError("테스트 쿼리를 입력하세요."); return; } setIsRetesting(true); clearMessages(); try { const results = await ragEngine.search(testQuery, 5); // Calculate accuracy based on expected answers const validExpectedAnswers = expectedAnswers.filter((answer) => answer.trim() ); let accuracy = 0; if (validExpectedAnswers.length > 0) { const foundMatches = results.filter((result) => validExpectedAnswers.some( (expected) => result.chunk.content .toLowerCase() .includes(expected.toLowerCase()) || expected .toLowerCase() .includes(result.chunk.content.toLowerCase().substring(0, 100)) ) ); accuracy = foundMatches.length / validExpectedAnswers.length; } const newTest: RetrievalTest = { id: `test_${Date.now()}`, query: testQuery, expectedAnswers: validExpectedAnswers, results: results.map((r) => ({ content: r.chunk.content, score: r.score, metadata: r.chunk.metadata, })), accuracy, timestamp: new Date().toISOString(), notes: testNotes, }; const updatedTests = [newTest, ...retrievalTests]; setRetrievalTests(updatedTests); localStorage.setItem("rag-retrieval-tests", JSON.stringify(updatedTests)); setSuccess(`검색 테스트 완료. 정확도: ${(accuracy * 100).toFixed(1)}%`); setIsTestModalOpen(false); resetTestForm(); } catch (err) { setError( `테스트 실행 실패: ${ err instanceof Error ? err.message : "알 수 없는 오류" }` ); } finally { setIsRetesting(false); } }; const resetTestForm = () => { setTestQuery(""); setExpectedAnswers([""]); setTestNotes(""); }; const addKnowledgeGap = () => { if (!gapCategory.trim() || !gapDescription.trim()) { setError("카테고리와 설명을 입력하세요."); return; } const newGap: KnowledgeGap = { id: `gap_${Date.now()}`, category: gapCategory, description: gapDescription, priority: gapPriority, suggestedActions: gapActions.filter((action) => action.trim()), relatedQueries: gapQueries.filter((query) => query.trim()), }; const updatedGaps = [newGap, ...knowledgeGaps]; setKnowledgeGaps(updatedGaps); localStorage.setItem("rag-knowledge-gaps", JSON.stringify(updatedGaps)); setSuccess("지식 격차가 추가되었습니다."); setIsGapModalOpen(false); resetGapForm(); }; const resetGapForm = () => { setGapCategory(""); setGapDescription(""); setGapPriority("medium"); setGapActions([""]); setGapQueries([""]); }; const deleteGap = (gapId: string) => { if (!confirm("이 지식 격차를 삭제하시겠습니까?")) return; const updatedGaps = knowledgeGaps.filter((gap) => gap.id !== gapId); setKnowledgeGaps(updatedGaps); localStorage.setItem("rag-knowledge-gaps", JSON.stringify(updatedGaps)); setSuccess("지식 격차가 삭제되었습니다."); }; const deleteTest = (testId: string) => { if (!confirm("이 테스트 결과를 삭제하시겠습니까?")) return; const updatedTests = retrievalTests.filter((test) => test.id !== testId); setRetrievalTests(updatedTests); localStorage.setItem("rag-retrieval-tests", JSON.stringify(updatedTests)); setSuccess("테스트 결과가 삭제되었습니다."); }; const triggerRetrain = async () => { if ( !confirm( "RAG 시스템을 재훈련하시겠습니까? 이 작업은 시간이 걸릴 수 있습니다." ) ) return; try { setIsAnalyzing(true); clearMessages(); // Trigger a refresh of embeddings await loadMetrics(); setSuccess("재훈련이 시작되었습니다. 백그라운드에서 처리됩니다."); onRetrainTriggered?.(); } catch (err) { setError( `재훈련 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}` ); } finally { setIsAnalyzing(false); } }; const analyzeKnowledgeGaps = async () => { setIsAnalyzing(true); clearMessages(); try { // Analyze low-performing queries and suggest gaps const lowAccuracyTests = retrievalTests.filter( (test) => test.accuracy < 0.7 ); if (lowAccuracyTests.length > 0) { const categories = [ ...new Set( lowAccuracyTests.map((test) => { // Extract category from query (simple heuristic) const query = test.query.toLowerCase(); if (query.includes("기술") || query.includes("개발")) return "기술 문서"; if (query.includes("사용법") || query.includes("방법")) return "사용 가이드"; if (query.includes("문제") || query.includes("해결")) return "문제 해결"; return "일반"; }) ), ]; const autoGaps: KnowledgeGap[] = categories.map((category) => ({ id: `auto_gap_${Date.now()}_${category}`, category, description: `${category} 관련 질문의 검색 정확도가 낮습니다.`, priority: "medium" as const, suggestedActions: [ `${category} 관련 문서 추가`, "기존 문서의 태그 및 메타데이터 개선", "검색 키워드 최적화", ], relatedQueries: lowAccuracyTests .filter((test) => test.query.toLowerCase().includes(category.split(" ")[0]) ) .map((test) => test.query), })); const existingGaps = [...knowledgeGaps]; const newGaps = autoGaps.filter( (gap) => !existingGaps.some((existing) => existing.category === gap.category) ); if (newGaps.length > 0) { const updatedGaps = [...newGaps, ...existingGaps]; setKnowledgeGaps(updatedGaps); localStorage.setItem( "rag-knowledge-gaps", JSON.stringify(updatedGaps) ); setSuccess( `${newGaps.length}개의 지식 격차가 자동으로 식별되었습니다.` ); } else { setSuccess("새로운 지식 격차가 발견되지 않았습니다."); } } else { setSuccess( "현재 검색 성능이 양호합니다. 추가 개선이 필요하지 않습니다." ); } } catch (err) { setError( `분석 실패: ${err instanceof Error ? err.message : "알 수 없는 오류"}` ); } finally { setIsAnalyzing(false); } }; const formatBytes = (bytes: number) => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString(); }; const getPriorityColor = (priority: string) => { switch (priority) { case "high": return "text-red-600 bg-red-50"; case "medium": return "text-yellow-600 bg-yellow-50"; case "low": return "text-green-600 bg-green-50"; default: return "text-gray-600 bg-gray-50"; } }; const getAccuracyColor = (accuracy: number) => { if (accuracy >= 0.8) return "text-green-600"; if (accuracy >= 0.6) return "text-yellow-600"; return "text-red-600"; }; return (
{/* Overview Metrics */}
{metrics?.totalDocuments || 0}

총 문서 수

{metrics?.totalChunks || 0}

총 청크 수

{metrics ? formatBytes(metrics.vectorStoreSize) : "0 B"}

벡터 저장소 크기

{retrievalTests.length > 0 ? `${( (retrievalTests.reduce( (sum, test) => sum + test.accuracy, 0 ) / retrievalTests.length) * 100 ).toFixed(1)}%` : "N/A"}

평균 검색 정확도

{/* Control Panel */} RAG 관리 제어판
{/* Knowledge Gaps */} 지식 격차 ({knowledgeGaps.length}개) {knowledgeGaps.length === 0 ? (

지식 격차가 없습니다. 분석을 실행하거나 수동으로 추가하세요.

) : (
{knowledgeGaps.map((gap) => (

{gap.category}

{gap.priority}

{gap.description}

{gap.suggestedActions.length > 0 && (
제안 조치:
    {gap.suggestedActions.map((action, idx) => (
  • {action}
  • ))}
)} {gap.relatedQueries.length > 0 && (
관련 질문:
{gap.relatedQueries .slice(0, 3) .map((query, idx) => ( {query} ))} {gap.relatedQueries.length > 3 && ( +{gap.relatedQueries.length - 3}개 더 )}
)}
))}
)}
{/* Retrieval Test Results */} 검색 테스트 결과 ({retrievalTests.length}개) {retrievalTests.length === 0 ? (

검색 테스트 결과가 없습니다. 새 테스트를 실행하세요.

) : (
{retrievalTests.slice(0, 5).map((test) => (

{test.query}

정확도: {(test.accuracy * 100).toFixed(1)}% 결과: {test.results.length}개 {formatDate(test.timestamp)}
{test.notes && (

{test.notes}

)}
검색 결과 미리보기:
{test.results.slice(0, 2).map((result, idx) => (
점수: {result.score.toFixed(3)}

{result.content}

))}
))} {retrievalTests.length > 5 && (

{retrievalTests.length - 5}개의 추가 테스트 결과가 있습니다.

)}
)}
{/* Query Analytics */} 자주 사용되는 질문 {frequentQueries.length === 0 ? (

분석할 질문 데이터가 없습니다.

) : (
{frequentQueries.map((query, idx) => (

{query.query}

사용 횟수: {query.count}회 | 평균 점수:{" "} {query.avgScore.toFixed(2)} | 마지막 사용:{" "} {formatDate(query.lastUsed)}
))}
)}
{/* Test Modal */} {isTestModalOpen && ( 검색 품질 테스트
setTestQuery(e.target.value)} placeholder="검색할 질문을 입력하세요" />
{expectedAnswers.map((answer, idx) => (
{ const newAnswers = [...expectedAnswers]; newAnswers[idx] = e.target.value; setExpectedAnswers(newAnswers); }} placeholder={`예상 답변 ${idx + 1}`} /> {idx === expectedAnswers.length - 1 && ( )} {expectedAnswers.length > 1 && ( )}
))}