init
This commit is contained in:
parent
33e09027b2
commit
94e8857f44
@ -92,17 +92,29 @@ class LangChainRAGService:
|
|||||||
def _setup_rag_chain(self):
|
def _setup_rag_chain(self):
|
||||||
"""RAG 체인 설정"""
|
"""RAG 체인 설정"""
|
||||||
try:
|
try:
|
||||||
# 프롬프트 템플릿
|
# 개선된 프롬프트 템플릿
|
||||||
prompt_template = """
|
prompt_template = """
|
||||||
다음 문서들을 참고하여 질문에 답변해주세요.
|
당신은 연구 문서 전문가입니다. 주어진 문서들을 바탕으로 사용자의 질문에 정확하고 상세하게 답변해주세요.
|
||||||
|
|
||||||
문서들:
|
**절대 지켜야 할 답변 규칙:**
|
||||||
{context}
|
1. 답변은 반드시 한국어로만 시작하세요
|
||||||
|
2. 영어 단어나 문장을 절대 사용하지 마세요
|
||||||
질문: {input}
|
3. <think>부터 </think>까지의 모든 내용을 절대 포함하지 마세요
|
||||||
|
4. 사고 과정, 분석 과정, 고민 과정을 전혀 보여주지 마세요
|
||||||
답변: 문서의 내용을 바탕으로 정확하고 상세하게 답변해주세요.
|
5. 바로 최종 답변만 제공하세요
|
||||||
"""
|
6. 문서의 내용을 정확히 파악하고 요약하여 답변하세요
|
||||||
|
7. 구체적인 절차나 방법이 있다면 단계별로 설명하세요
|
||||||
|
8. 중요한 정보나 주의사항이 있다면 강조하세요
|
||||||
|
9. 문서에 없는 내용은 추측하지 말고 "문서에 명시되지 않음"이라고 하세요
|
||||||
|
10. 마크다운 형식을 사용하여 구조화된 답변을 제공하세요
|
||||||
|
|
||||||
|
**참조 문서:**
|
||||||
|
{context}
|
||||||
|
|
||||||
|
**사용자 질문:** {input}
|
||||||
|
|
||||||
|
**답변:** 위 문서의 내용을 바탕으로 질문에 대한 구체적이고 실용적인 답변을 마크다운 형식으로 바로 제공해주세요. 반드시 한국어로 시작하고, 영어나 <think> 태그는 절대 포함하지 마세요.
|
||||||
|
"""
|
||||||
|
|
||||||
prompt = PromptTemplate(
|
prompt = PromptTemplate(
|
||||||
template=prompt_template,
|
template=prompt_template,
|
||||||
@ -171,33 +183,133 @@ class LangChainRAGService:
|
|||||||
def generate_answer(self, question: str) -> Dict[str, Any]:
|
def generate_answer(self, question: str) -> Dict[str, Any]:
|
||||||
"""RAG를 통한 답변 생성"""
|
"""RAG를 통한 답변 생성"""
|
||||||
try:
|
try:
|
||||||
# 간단한 유사 문서 검색으로 시작
|
# RAG 체인을 사용하여 답변 생성
|
||||||
|
if self.qa_chain:
|
||||||
|
logger.info(f"🤖 LLM을 사용한 RAG 답변 생성 시작: {question}")
|
||||||
|
result = self.qa_chain.invoke({"input": question})
|
||||||
|
|
||||||
|
# 참조 문서 정보 추출
|
||||||
|
references = []
|
||||||
|
source_documents = result.get("context", [])
|
||||||
|
|
||||||
|
for doc in source_documents:
|
||||||
|
if hasattr(doc, 'metadata') and doc.metadata:
|
||||||
|
filename = doc.metadata.get('filename', 'Unknown')
|
||||||
|
file_id = doc.metadata.get('file_id', 'unknown')
|
||||||
|
chunk_index = doc.metadata.get('chunk_index', 0)
|
||||||
|
page_number = chunk_index + 1
|
||||||
|
references.append(f"{filename}::{file_id} [p{page_number}]")
|
||||||
|
|
||||||
|
# <think> 태그 및 영어 사고 과정 제거 (강화된 버전)
|
||||||
|
answer = result.get("answer", "답변을 생성할 수 없습니다.")
|
||||||
|
import re
|
||||||
|
|
||||||
|
# <think>부터 </think>까지의 모든 내용 제거 (대소문자 구분 없음)
|
||||||
|
answer = re.sub(r'<think>.*?</think>', '', answer, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||||
|
# </think> 태그만 있는 경우도 제거
|
||||||
|
answer = re.sub(r'</think>', '', answer, flags=re.IGNORECASE).strip()
|
||||||
|
# <think> 태그만 있는 경우도 제거
|
||||||
|
answer = re.sub(r'<think>', '', answer, flags=re.IGNORECASE).strip()
|
||||||
|
|
||||||
|
# 영어 사고 과정 완전 제거 (극강화 버전)
|
||||||
|
lines = answer.split('\n')
|
||||||
|
filtered_lines = []
|
||||||
|
korean_found = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
filtered_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 한국어가 포함된 줄이 나오면 korean_found = True
|
||||||
|
if re.search(r'[가-힣]', line):
|
||||||
|
korean_found = True
|
||||||
|
|
||||||
|
# 한국어가 발견되기 전까지는 모든 영어 줄 무조건 제거
|
||||||
|
if not korean_found:
|
||||||
|
if re.match(r'^[A-Za-z]', line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 한국어가 발견된 후에도 영어 사고 과정 제거
|
||||||
|
if re.match(r'^[A-Za-z]', line):
|
||||||
|
# 모든 영어 패턴 제거 (더 많은 패턴 추가)
|
||||||
|
if any(phrase in line.lower() for phrase in [
|
||||||
|
'let me', 'i need to', 'i should', 'i will', 'i can',
|
||||||
|
'the answer should', 'make sure', 'avoid any',
|
||||||
|
'structure the answer', 'break down', 'organize',
|
||||||
|
'tag or any thinking', 'so i need to focus',
|
||||||
|
'focus on the main content', 'documents',
|
||||||
|
'thinking process', 'internal thought',
|
||||||
|
'i need to focus', 'main content', 'tag', 'thinking',
|
||||||
|
'tag or any', 'so i need', 'focus on', 'main content of',
|
||||||
|
'documents', 'thinking', 'process', 'internal'
|
||||||
|
]):
|
||||||
|
continue
|
||||||
|
# 영어 문장이지만 중요한 내용이 아닌 경우 제거
|
||||||
|
if len(line) > 10 and any(word in line.lower() for word in [
|
||||||
|
'thinking', 'process', 'structure', 'format', 'markdown',
|
||||||
|
'tag', 'focus', 'content', 'documents', 'internal',
|
||||||
|
'answer', 'should', 'make', 'sure', 'avoid'
|
||||||
|
]):
|
||||||
|
continue
|
||||||
|
# 짧은 영어 문장도 제거 (사고 과정일 가능성)
|
||||||
|
if len(line) < 200 and not re.search(r'[가-힣]', line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
filtered_lines.append(line)
|
||||||
|
|
||||||
|
answer = '\n'.join(filtered_lines).strip()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"answer": answer,
|
||||||
|
"references": references,
|
||||||
|
"source_documents": source_documents
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"✅ LLM RAG 답변 생성 완료: {len(references)}개 참조")
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
# RAG 체인이 없는 경우 폴백
|
||||||
|
logger.warning("⚠️ RAG 체인이 초기화되지 않음. 폴백 모드로 전환")
|
||||||
|
return self._generate_fallback_answer(question)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ RAG 답변 생성 실패: {e}")
|
||||||
|
# 오류 시 폴백 답변 생성
|
||||||
|
return self._generate_fallback_answer(question)
|
||||||
|
|
||||||
|
def _generate_fallback_answer(self, question: str) -> Dict[str, Any]:
|
||||||
|
"""폴백 답변 생성 (LLM 없이)"""
|
||||||
|
try:
|
||||||
|
# 유사 문서 검색
|
||||||
similar_docs = self.search_similar_documents(question, k=3)
|
similar_docs = self.search_similar_documents(question, k=3)
|
||||||
|
|
||||||
if not similar_docs:
|
if not similar_docs:
|
||||||
return {
|
return {
|
||||||
"answer": "죄송합니다. 관련 문서를 찾을 수 없습니다.",
|
"answer": "죄송합니다. 관련 문서를 찾을 수 없습니다. 다른 키워드로 검색해보시거나 문서를 업로드해주세요.",
|
||||||
"references": ["문서 없음"],
|
"references": [],
|
||||||
"source_documents": []
|
"source_documents": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# 문서 내용을 기반으로 간단한 답변 생성
|
# 문서 내용 요약
|
||||||
context_text = ""
|
context_summary = ""
|
||||||
references = []
|
references = []
|
||||||
|
|
||||||
for i, doc in enumerate(similar_docs):
|
for i, doc in enumerate(similar_docs):
|
||||||
context_text += f"\n문서 {i+1}:\n{doc.page_content[:500]}...\n"
|
# 문서 내용의 핵심 부분 추출 (처음 300자)
|
||||||
|
content_preview = doc.page_content[:300].replace('\n', ' ').strip()
|
||||||
|
context_summary += f"\n• {content_preview}...\n"
|
||||||
|
|
||||||
if hasattr(doc, 'metadata') and doc.metadata:
|
if hasattr(doc, 'metadata') and doc.metadata:
|
||||||
filename = doc.metadata.get('filename', 'Unknown')
|
filename = doc.metadata.get('filename', 'Unknown')
|
||||||
file_id = doc.metadata.get('file_id', 'unknown')
|
file_id = doc.metadata.get('file_id', 'unknown')
|
||||||
chunk_index = doc.metadata.get('chunk_index', 0)
|
chunk_index = doc.metadata.get('chunk_index', 0)
|
||||||
# 페이지 번호는 청크 인덱스를 기반으로 추정 (실제로는 더 정확한 방법 필요)
|
|
||||||
page_number = chunk_index + 1
|
page_number = chunk_index + 1
|
||||||
references.append(f"{filename}::{file_id} [p{page_number}]")
|
references.append(f"{filename}::{file_id} [p{page_number}]")
|
||||||
|
|
||||||
# 간단한 답변 생성 (LLM 없이)
|
# 간단한 답변 생성
|
||||||
answer = f"질문하신 '{question}'에 대한 관련 문서를 찾았습니다.\n\n참조 문서에서 관련 내용을 확인할 수 있습니다."
|
answer = f"질문하신 '{question}'에 대한 관련 문서를 찾았습니다.\n\n참조 문서에서 관련 내용을 확인할 수 있습니다:\n{context_summary}"
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"answer": answer,
|
"answer": answer,
|
||||||
@ -205,15 +317,14 @@ class LangChainRAGService:
|
|||||||
"source_documents": similar_docs
|
"source_documents": similar_docs
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"✅ RAG 답변 생성 완료: {len(references)}개 참조")
|
logger.info(f"✅ 폴백 답변 생성 완료: {len(references)}개 참조")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ RAG 답변 생성 실패: {e}")
|
logger.error(f"❌ 폴백 답변 생성 실패: {e}")
|
||||||
# 오류 시 기본 응답 반환
|
|
||||||
return {
|
return {
|
||||||
"answer": "죄송합니다. 현재 시스템 오류로 인해 답변을 생성할 수 없습니다.",
|
"answer": "죄송합니다. 현재 시스템 오류로 인해 답변을 생성할 수 없습니다. 잠시 후 다시 시도해주세요.",
|
||||||
"references": ["시스템 오류"],
|
"references": [],
|
||||||
"source_documents": []
|
"source_documents": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,20 +3,27 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"@types/node": "^20.0.0",
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@types/node": "^20.0.0",
|
"autoprefixer": "^10.4.0",
|
||||||
"framer-motion": "^10.16.0",
|
"framer-motion": "^10.16.0",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react-pdf": "^10.1.0",
|
|
||||||
"pdfjs-dist": "^5.3.93",
|
"pdfjs-dist": "^5.3.93",
|
||||||
|
"postcss": "^8.4.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-pdf": "^10.1.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"rehype-highlight": "^7.0.2",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark-breaks": "^4.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
"autoprefixer": "^10.4.0",
|
"typescript": "^4.9.5"
|
||||||
"postcss": "^8.4.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
import ChatInterface from './components/ChatInterface';
|
import ChatInterface from './components/ChatInterface';
|
||||||
import LoginModal from './components/LoginModal';
|
import LoginModal from './components/LoginModal';
|
||||||
import FileUploadModal from './components/FileUploadModal';
|
import FileUploadModal from './components/FileUploadModal';
|
||||||
|
import PDFViewer from './components/PDFViewer';
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
import { ChatProvider } from './contexts/ChatContext';
|
import { ChatProvider } from './contexts/ChatContext';
|
||||||
import { FileProvider } from './contexts/FileContext';
|
import { FileProvider } from './contexts/FileContext';
|
||||||
@ -12,6 +13,12 @@ function AppContent() {
|
|||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const [showLogin, setShowLogin] = useState(false);
|
const [showLogin, setShowLogin] = useState(false);
|
||||||
const [showFileUpload, setShowFileUpload] = useState(false);
|
const [showFileUpload, setShowFileUpload] = useState(false);
|
||||||
|
const [showPDFViewer, setShowPDFViewer] = useState(false);
|
||||||
|
const [pdfViewerData, setPdfViewerData] = useState<{
|
||||||
|
fileId: string;
|
||||||
|
filename: string;
|
||||||
|
pageNumber: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
|
||||||
const handleFileUploadClick = () => {
|
const handleFileUploadClick = () => {
|
||||||
@ -25,6 +32,22 @@ function AppContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePDFView = (fileId: string, filename: string) => {
|
||||||
|
setPdfViewerData({
|
||||||
|
fileId,
|
||||||
|
filename,
|
||||||
|
pageNumber: 1
|
||||||
|
});
|
||||||
|
setShowPDFViewer(true);
|
||||||
|
setShowFileUpload(false); // 파일 업로드 모달 닫기
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePDFViewerClose = () => {
|
||||||
|
setShowPDFViewer(false);
|
||||||
|
setPdfViewerData(null);
|
||||||
|
setShowFileUpload(true); // 파일 업로드 모달 다시 열기
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 flex flex-col">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 flex flex-col">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
@ -77,6 +100,16 @@ function AppContent() {
|
|||||||
{showFileUpload && (
|
{showFileUpload && (
|
||||||
<FileUploadModal
|
<FileUploadModal
|
||||||
onClose={() => setShowFileUpload(false)}
|
onClose={() => setShowFileUpload(false)}
|
||||||
|
onPDFView={handlePDFView}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showPDFViewer && pdfViewerData && (
|
||||||
|
<PDFViewer
|
||||||
|
fileId={pdfViewerData.fileId}
|
||||||
|
filename={pdfViewerData.filename}
|
||||||
|
pageNumber={pdfViewerData.pageNumber}
|
||||||
|
onClose={handlePDFViewerClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import { X, Upload, Trash2, Search } from 'lucide-react';
|
|||||||
|
|
||||||
interface FileUploadModalProps {
|
interface FileUploadModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onPDFView: (fileId: string, filename: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose }) => {
|
const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose, onPDFView }) => {
|
||||||
const { files, uploadFile, deleteFile, refreshFiles, searchFiles, isLoading } = useFiles();
|
const { files, uploadFile, deleteFile, refreshFiles, searchFiles, isLoading } = useFiles();
|
||||||
const [dragActive, setDragActive] = useState(false);
|
const [dragActive, setDragActive] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@ -252,6 +253,10 @@ const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDocumentDoubleClick = (fileId: string, filename: string) => {
|
||||||
|
onPDFView(fileId, filename);
|
||||||
|
};
|
||||||
|
|
||||||
const showTooltip = (content: string, e: React.MouseEvent) => {
|
const showTooltip = (content: string, e: React.MouseEvent) => {
|
||||||
setTooltip({
|
setTooltip({
|
||||||
show: true,
|
show: true,
|
||||||
@ -483,10 +488,12 @@ const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose }) => {
|
|||||||
{file.id}
|
{file.id}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="col-span-5 text-sm text-gray-800 truncate cursor-help"
|
className="col-span-5 text-sm text-gray-800 truncate cursor-pointer hover:text-blue-600 hover:underline transition-colors"
|
||||||
onMouseEnter={(e) => showTooltip(file.filename, e)}
|
onMouseEnter={(e) => showTooltip(file.filename, e)}
|
||||||
onMouseLeave={hideTooltip}
|
onMouseLeave={hideTooltip}
|
||||||
onMouseMove={(e) => showTooltip(file.filename, e)}
|
onMouseMove={(e) => showTooltip(file.filename, e)}
|
||||||
|
onDoubleClick={() => handleDocumentDoubleClick(file.id, file.filename)}
|
||||||
|
title="더블클릭하여 PDF 뷰어로 열기"
|
||||||
>
|
>
|
||||||
{file.filename}
|
{file.filename}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { User, Bot, FileText } from 'lucide-react';
|
import { User, FileText } from 'lucide-react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkBreaks from 'remark-breaks';
|
||||||
import { Message } from '../contexts/ChatContext';
|
import { Message } from '../contexts/ChatContext';
|
||||||
import PDFViewer from './PDFViewer';
|
import PDFViewer from './PDFViewer';
|
||||||
|
|
||||||
@ -88,28 +91,82 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
|||||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||||
isUser
|
isUser
|
||||||
? 'bg-gradient-to-r from-blue-500 to-purple-500'
|
? 'bg-gradient-to-r from-blue-500 to-purple-500'
|
||||||
: 'bg-gradient-to-r from-gray-500 to-gray-600'
|
: 'bg-transparent'
|
||||||
}`}>
|
}`}>
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<User className="w-4 h-4 text-white" />
|
<User className="w-4 h-4 text-white" />
|
||||||
) : (
|
) : (
|
||||||
<Bot className="w-4 h-4 text-white" />
|
<img
|
||||||
|
src="/images/woongtalk_bgremove.png"
|
||||||
|
alt="연구QA 챗봇"
|
||||||
|
className="w-8 h-8"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 메시지 내용 */}
|
{/* 메시지 내용 */}
|
||||||
<div className={`rounded-2xl px-4 py-3 flex-1 ${
|
<div className={`rounded-2xl px-4 py-3 flex-1 border border-gray-200 ${
|
||||||
isUser
|
isUser
|
||||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white'
|
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white'
|
||||||
: 'bg-gray-100 text-gray-800'
|
: 'bg-gray-100 text-gray-800'
|
||||||
}`}>
|
}`}>
|
||||||
<div className="whitespace-pre-wrap break-words">
|
{isUser ? (
|
||||||
{message.content}
|
<div className="whitespace-pre-wrap break-words">
|
||||||
</div>
|
{message.content}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="markdown-content">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||||
|
components={{
|
||||||
|
// pre 태그를 완전히 제거하고 일반 div로 대체
|
||||||
|
pre: ({ children }) => <div>{children}</div>,
|
||||||
|
// code 태그 처리
|
||||||
|
code: ({ children, className }) => {
|
||||||
|
const isInline = !className;
|
||||||
|
return isInline ? (
|
||||||
|
<code className="bg-gray-200 px-2 py-1 rounded text-sm font-mono text-gray-800">{children}</code>
|
||||||
|
) : (
|
||||||
|
<div className="bg-gray-100 text-gray-800 p-4 rounded-lg overflow-x-auto mb-4 font-mono text-sm">{children}</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// 모든 마크다운 요소에 대한 커스텀 스타일링
|
||||||
|
h1: ({ children }) => <h1 className="text-2xl font-bold mb-4 text-gray-800 border-b border-gray-200 pb-2">{children}</h1>,
|
||||||
|
h2: ({ children }) => <h2 className="text-xl font-bold mb-3 text-gray-800 mt-6">{children}</h2>,
|
||||||
|
h3: ({ children }) => <h3 className="text-lg font-bold mb-2 text-gray-800 mt-4">{children}</h3>,
|
||||||
|
h4: ({ children }) => <h4 className="text-base font-bold mb-2 text-gray-800 mt-3">{children}</h4>,
|
||||||
|
p: ({ children }) => <p className="mb-3 text-gray-700 leading-relaxed">{children}</p>,
|
||||||
|
ul: ({ children }) => <ul className="list-disc list-inside mb-4 text-gray-700 space-y-1 ml-4">{children}</ul>,
|
||||||
|
ol: ({ children }) => <ol className="list-decimal list-inside mb-4 text-gray-700 space-y-1 ml-4">{children}</ol>,
|
||||||
|
li: ({ children }) => <li className="mb-1">{children}</li>,
|
||||||
|
strong: ({ children }) => <strong className="font-bold text-gray-800">{children}</strong>,
|
||||||
|
em: ({ children }) => <em className="italic text-gray-700">{children}</em>,
|
||||||
|
blockquote: ({ children }) => <blockquote className="border-l-4 border-blue-300 pl-4 italic text-gray-600 bg-blue-50 py-2 rounded-r mb-4">{children}</blockquote>,
|
||||||
|
table: ({ children }) => <div className="overflow-x-auto mb-4"><table className="min-w-full border-collapse border border-gray-300">{children}</table></div>,
|
||||||
|
thead: ({ children }) => <thead className="bg-gray-50">{children}</thead>,
|
||||||
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||||
|
tr: ({ children }) => <tr className="border-b border-gray-200 hover:bg-gray-50">{children}</tr>,
|
||||||
|
th: ({ children }) => <th className="border border-gray-300 px-4 py-2 text-left font-semibold text-gray-800 bg-gray-100">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="border border-gray-300 px-4 py-2 text-gray-700">{children}</td>,
|
||||||
|
hr: () => <hr className="my-6 border-gray-300" />,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 소스 정보 */}
|
{/* 소스 정보 */}
|
||||||
{!isUser && message.sources && message.sources.length > 0 && (
|
{!isUser && message.sources && message.sources.length > 0 && (
|
||||||
<div className="mt-3 pt-3 border-t border-gray-300/30">
|
<div className="mt-3 pt-3" style={{
|
||||||
|
borderTop: 'none !important',
|
||||||
|
border: 'none !important',
|
||||||
|
borderBottom: 'none !important',
|
||||||
|
borderLeft: 'none !important',
|
||||||
|
borderRight: 'none !important',
|
||||||
|
outline: 'none !important',
|
||||||
|
boxShadow: 'none !important'
|
||||||
|
}}>
|
||||||
<div className="flex items-center space-x-1 text-sm opacity-75">
|
<div className="flex items-center space-x-1 text-sm opacity-75">
|
||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
<span>참조 문서:</span>
|
<span>참조 문서:</span>
|
||||||
|
|||||||
@ -5,8 +5,12 @@ const TypingIndicator: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-8 h-8 rounded-full bg-gradient-to-r from-gray-500 to-gray-600 flex items-center justify-center">
|
<div className="w-8 h-8 rounded-full flex items-center justify-center">
|
||||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
<img
|
||||||
|
src="/images/woongtalk_bgremove.png"
|
||||||
|
alt="연구QA 챗봇"
|
||||||
|
className="w-8 h-8"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 rounded-2xl px-4 py-3">
|
<div className="bg-gray-100 rounded-2xl px-4 py-3">
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
|
|||||||
@ -164,3 +164,345 @@ code {
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transform-origin: 0% 0%;
|
transform-origin: 0% 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 마크다운 콘텐츠 스타일 */
|
||||||
|
.markdown-content {
|
||||||
|
max-width: 100%;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1,
|
||||||
|
.markdown-content h2,
|
||||||
|
.markdown-content h3,
|
||||||
|
.markdown-content h4,
|
||||||
|
.markdown-content h5,
|
||||||
|
.markdown-content h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul,
|
||||||
|
.markdown-content ol {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content em {
|
||||||
|
font-style: italic;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: #f9fafb;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #4b5563;
|
||||||
|
background-color: #eff6ff;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0 0.25rem 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th,
|
||||||
|
.markdown-content td {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content tr:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none !important;
|
||||||
|
border-top: none !important;
|
||||||
|
margin: 2rem 0;
|
||||||
|
display: none !important; /* hr 요소 완전 숨김 */
|
||||||
|
height: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모든 가능한 선 제거 */
|
||||||
|
.markdown-content * {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content > *:first-child {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 참조 문서 섹션의 모든 선 제거 */
|
||||||
|
.mt-3.pt-3 {
|
||||||
|
border-top: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모든 div 요소의 상단 테두리 제거 */
|
||||||
|
div {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 특정 클래스의 테두리 제거 */
|
||||||
|
.border-t {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모든 가능한 선과 배경 제거 */
|
||||||
|
* {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 메시지 버블의 모든 테두리 제거 */
|
||||||
|
.rounded-2xl {
|
||||||
|
border: none !important;
|
||||||
|
border-top: none !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 마크다운 콘텐츠 스타일 */
|
||||||
|
.markdown-content {
|
||||||
|
border: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
line-height: 1.6 !important;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 마크다운 요소들 기본 스타일 */
|
||||||
|
.markdown-content h1 {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
margin: 1rem 0 0.5rem 0 !important;
|
||||||
|
border-bottom: 2px solid #E5E7EB !important;
|
||||||
|
padding-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h2 {
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
margin: 1.5rem 0 0.5rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h3 {
|
||||||
|
font-size: 1.125rem !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
margin: 1.25rem 0 0.5rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h4 {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
margin: 1rem 0 0.5rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
margin: 0.5rem 0 !important;
|
||||||
|
line-height: 1.6 !important;
|
||||||
|
color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul {
|
||||||
|
margin: 0.5rem 0 !important;
|
||||||
|
padding-left: 1.5rem !important;
|
||||||
|
list-style-type: disc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol {
|
||||||
|
margin: 0.5rem 0 !important;
|
||||||
|
padding-left: 1.5rem !important;
|
||||||
|
list-style-type: decimal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
margin: 0.25rem 0 !important;
|
||||||
|
color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content strong {
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content em {
|
||||||
|
font-style: italic !important;
|
||||||
|
color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
background-color: #F3F4F6 !important;
|
||||||
|
padding: 0.125rem 0.25rem !important;
|
||||||
|
border-radius: 0.25rem !important;
|
||||||
|
font-family: 'Courier New', monospace !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 마크다운 내용이 pre 태그로 감싸지지 않도록 설정 */
|
||||||
|
.markdown-content {
|
||||||
|
white-space: normal !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 전체 마크다운을 감싸는 pre 태그의 스타일을 일반 div처럼 변경 */
|
||||||
|
.markdown-content > pre {
|
||||||
|
white-space: normal !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 실제 코드 블록에만 적용되는 pre 태그 스타일 */
|
||||||
|
.markdown-content pre {
|
||||||
|
background-color: #F3F4F6 !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
padding: 1rem !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
margin: 1rem 0 !important;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pre 태그 내부의 코드 블록만 스타일 적용 */
|
||||||
|
.markdown-content pre code {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
border-left: 4px solid #3B82F6 !important;
|
||||||
|
padding-left: 1rem !important;
|
||||||
|
margin: 1rem 0 !important;
|
||||||
|
font-style: italic !important;
|
||||||
|
color: #6B7280 !important;
|
||||||
|
background-color: #EFF6FF !important;
|
||||||
|
padding: 0.5rem 1rem !important;
|
||||||
|
border-radius: 0 0.25rem 0.25rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
margin: 1rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th {
|
||||||
|
border: 1px solid #D1D5DB !important;
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
text-align: left !important;
|
||||||
|
background-color: #F9FAFB !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content td {
|
||||||
|
border: 1px solid #D1D5DB !important;
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
text-align: left !important;
|
||||||
|
color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none !important;
|
||||||
|
border-top: 1px solid #E5E7EB !important;
|
||||||
|
margin: 2rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 참조 문서 섹션의 모든 선과 배경 제거 */
|
||||||
|
.mt-3.pt-3 {
|
||||||
|
border: none !important;
|
||||||
|
border-top: none !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-right: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user