init
This commit is contained in:
parent
33e09027b2
commit
94e8857f44
@ -92,16 +92,28 @@ class LangChainRAGService:
|
||||
def _setup_rag_chain(self):
|
||||
"""RAG 체인 설정"""
|
||||
try:
|
||||
# 프롬프트 템플릿
|
||||
# 개선된 프롬프트 템플릿
|
||||
prompt_template = """
|
||||
다음 문서들을 참고하여 질문에 답변해주세요.
|
||||
당신은 연구 문서 전문가입니다. 주어진 문서들을 바탕으로 사용자의 질문에 정확하고 상세하게 답변해주세요.
|
||||
|
||||
문서들:
|
||||
**절대 지켜야 할 답변 규칙:**
|
||||
1. 답변은 반드시 한국어로만 시작하세요
|
||||
2. 영어 단어나 문장을 절대 사용하지 마세요
|
||||
3. <think>부터 </think>까지의 모든 내용을 절대 포함하지 마세요
|
||||
4. 사고 과정, 분석 과정, 고민 과정을 전혀 보여주지 마세요
|
||||
5. 바로 최종 답변만 제공하세요
|
||||
6. 문서의 내용을 정확히 파악하고 요약하여 답변하세요
|
||||
7. 구체적인 절차나 방법이 있다면 단계별로 설명하세요
|
||||
8. 중요한 정보나 주의사항이 있다면 강조하세요
|
||||
9. 문서에 없는 내용은 추측하지 말고 "문서에 명시되지 않음"이라고 하세요
|
||||
10. 마크다운 형식을 사용하여 구조화된 답변을 제공하세요
|
||||
|
||||
**참조 문서:**
|
||||
{context}
|
||||
|
||||
질문: {input}
|
||||
**사용자 질문:** {input}
|
||||
|
||||
답변: 문서의 내용을 바탕으로 정확하고 상세하게 답변해주세요.
|
||||
**답변:** 위 문서의 내용을 바탕으로 질문에 대한 구체적이고 실용적인 답변을 마크다운 형식으로 바로 제공해주세요. 반드시 한국어로 시작하고, 영어나 <think> 태그는 절대 포함하지 마세요.
|
||||
"""
|
||||
|
||||
prompt = PromptTemplate(
|
||||
@ -171,33 +183,133 @@ class LangChainRAGService:
|
||||
def generate_answer(self, question: str) -> Dict[str, Any]:
|
||||
"""RAG를 통한 답변 생성"""
|
||||
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)
|
||||
|
||||
if not similar_docs:
|
||||
return {
|
||||
"answer": "죄송합니다. 관련 문서를 찾을 수 없습니다.",
|
||||
"references": ["문서 없음"],
|
||||
"answer": "죄송합니다. 관련 문서를 찾을 수 없습니다. 다른 키워드로 검색해보시거나 문서를 업로드해주세요.",
|
||||
"references": [],
|
||||
"source_documents": []
|
||||
}
|
||||
|
||||
# 문서 내용을 기반으로 간단한 답변 생성
|
||||
context_text = ""
|
||||
# 문서 내용 요약
|
||||
context_summary = ""
|
||||
references = []
|
||||
|
||||
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:
|
||||
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}]")
|
||||
|
||||
# 간단한 답변 생성 (LLM 없이)
|
||||
answer = f"질문하신 '{question}'에 대한 관련 문서를 찾았습니다.\n\n참조 문서에서 관련 내용을 확인할 수 있습니다."
|
||||
# 간단한 답변 생성
|
||||
answer = f"질문하신 '{question}'에 대한 관련 문서를 찾았습니다.\n\n참조 문서에서 관련 내용을 확인할 수 있습니다:\n{context_summary}"
|
||||
|
||||
response = {
|
||||
"answer": answer,
|
||||
@ -205,15 +317,14 @@ class LangChainRAGService:
|
||||
"source_documents": similar_docs
|
||||
}
|
||||
|
||||
logger.info(f"✅ RAG 답변 생성 완료: {len(references)}개 참조")
|
||||
logger.info(f"✅ 폴백 답변 생성 완료: {len(references)}개 참조")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ RAG 답변 생성 실패: {e}")
|
||||
# 오류 시 기본 응답 반환
|
||||
logger.error(f"❌ 폴백 답변 생성 실패: {e}")
|
||||
return {
|
||||
"answer": "죄송합니다. 현재 시스템 오류로 인해 답변을 생성할 수 없습니다.",
|
||||
"references": ["시스템 오류"],
|
||||
"answer": "죄송합니다. 현재 시스템 오류로 인해 답변을 생성할 수 없습니다. 잠시 후 다시 시도해주세요.",
|
||||
"references": [],
|
||||
"source_documents": []
|
||||
}
|
||||
|
||||
|
||||
@ -3,20 +3,27 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"framer-motion": "^10.16.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react-pdf": "^10.1.0",
|
||||
"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",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.4.0"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import ChatInterface from './components/ChatInterface';
|
||||
import LoginModal from './components/LoginModal';
|
||||
import FileUploadModal from './components/FileUploadModal';
|
||||
import PDFViewer from './components/PDFViewer';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { ChatProvider } from './contexts/ChatContext';
|
||||
import { FileProvider } from './contexts/FileContext';
|
||||
@ -12,6 +13,12 @@ function AppContent() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [showLogin, setShowLogin] = 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 = () => {
|
||||
@ -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 (
|
||||
<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 && (
|
||||
<FileUploadModal
|
||||
onClose={() => setShowFileUpload(false)}
|
||||
onPDFView={handlePDFView}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPDFViewer && pdfViewerData && (
|
||||
<PDFViewer
|
||||
fileId={pdfViewerData.fileId}
|
||||
filename={pdfViewerData.filename}
|
||||
pageNumber={pdfViewerData.pageNumber}
|
||||
onClose={handlePDFViewerClose}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@ -5,9 +5,10 @@ import { X, Upload, Trash2, Search } from 'lucide-react';
|
||||
|
||||
interface FileUploadModalProps {
|
||||
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 [dragActive, setDragActive] = useState(false);
|
||||
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) => {
|
||||
setTooltip({
|
||||
show: true,
|
||||
@ -483,10 +488,12 @@ const FileUploadModal: React.FC<FileUploadModalProps> = ({ onClose }) => {
|
||||
{file.id}
|
||||
</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)}
|
||||
onMouseLeave={hideTooltip}
|
||||
onMouseMove={(e) => showTooltip(file.filename, e)}
|
||||
onDoubleClick={() => handleDocumentDoubleClick(file.id, file.filename)}
|
||||
title="더블클릭하여 PDF 뷰어로 열기"
|
||||
>
|
||||
{file.filename}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
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 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 ${
|
||||
isUser
|
||||
? 'bg-gradient-to-r from-blue-500 to-purple-500'
|
||||
: 'bg-gradient-to-r from-gray-500 to-gray-600'
|
||||
: 'bg-transparent'
|
||||
}`}>
|
||||
{isUser ? (
|
||||
<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 className={`rounded-2xl px-4 py-3 flex-1 ${
|
||||
<div className={`rounded-2xl px-4 py-3 flex-1 border border-gray-200 ${
|
||||
isUser
|
||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{isUser ? (
|
||||
<div className="whitespace-pre-wrap break-words">
|
||||
{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 && (
|
||||
<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">
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>참조 문서:</span>
|
||||
|
||||
@ -5,8 +5,12 @@ const TypingIndicator: React.FC = () => {
|
||||
return (
|
||||
<div className="flex justify-start">
|
||||
<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-4 h-4 bg-white rounded-full"></div>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center">
|
||||
<img
|
||||
src="/images/woongtalk_bgremove.png"
|
||||
alt="연구QA 챗봇"
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-gray-100 rounded-2xl px-4 py-3">
|
||||
<div className="flex space-x-1">
|
||||
|
||||
@ -164,3 +164,345 @@ code {
|
||||
transform: scale(1);
|
||||
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