Spaces:
Runtime error
Runtime error
add files
Browse files- .gitignore +27 -0
- config/__init__.py +0 -0
- config/settings.py +43 -0
- core/__init__.py +0 -0
- core/chunker.py +73 -0
- core/embedder.py +60 -0
- core/generator.py +193 -0
- core/pdf_loader.py +58 -0
- core/retriever.py +67 -0
- core/vectordb.py +100 -0
- ui/__init__.py +0 -0
- ui/components.py +210 -0
- ui/styles.py +509 -0
- utils/__init__.py +0 -0
- utils/export_utils.py +0 -0
- utils/helpers.py +69 -0
- utils/logger.py +41 -0
- utils/pdf_utils.py +57 -0
- utils/session_manager.py +0 -0
- utils/sidebar_components.py +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Keys
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Python
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*$py.class
|
| 8 |
+
*.so
|
| 9 |
+
.Python
|
| 10 |
+
venv/
|
| 11 |
+
ENV/
|
| 12 |
+
env/
|
| 13 |
+
|
| 14 |
+
# Data
|
| 15 |
+
data/uploads/*.pdf
|
| 16 |
+
data/chroma_db/
|
| 17 |
+
|
| 18 |
+
# IDE
|
| 19 |
+
.vscode/
|
| 20 |
+
.idea/
|
| 21 |
+
*.swp
|
| 22 |
+
*.swo
|
| 23 |
+
|
| 24 |
+
# OS
|
| 25 |
+
.DS_Store
|
| 26 |
+
Thumbs.db
|
| 27 |
+
|
config/__init__.py
ADDED
|
File without changes
|
config/settings.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# config/settings.py
|
| 2 |
+
"""์ค์ ํ์ผ"""
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 9 |
+
# ๋ธ๋๋ฉ (โ
PROBIN)
|
| 10 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 11 |
+
APP_NAME = "PROBIN"
|
| 12 |
+
APP_SUBTITLE = "Experience Intelligent Document Analysis with AI"
|
| 13 |
+
APP_ICON = "๐ฎ" # ์์ ๊ตฌ์ฌ
|
| 14 |
+
|
| 15 |
+
# UI ์ค์
|
| 16 |
+
SHOW_STATS = False # ํต๊ณ ์จ๊น
|
| 17 |
+
PDF_HEIGHT = "80vh"
|
| 18 |
+
|
| 19 |
+
# API Keys
|
| 20 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 21 |
+
XAI_API_KEY = os.getenv("XAI_API_KEY")
|
| 22 |
+
|
| 23 |
+
if not OPENAI_API_KEY or not XAI_API_KEY:
|
| 24 |
+
raise ValueError("โ .env ํ์ผ์ API Keys๋ฅผ ์ค์ ํ์ธ์!")
|
| 25 |
+
|
| 26 |
+
# ์๋ฒ ๋ฉ ์ค์
|
| 27 |
+
EMBEDDING_MODEL = "text-embedding-3-small"
|
| 28 |
+
EMBEDDING_DIMENSION = 1536
|
| 29 |
+
|
| 30 |
+
# ์ฒญํน ์ค์
|
| 31 |
+
CHUNK_SIZE = 800
|
| 32 |
+
CHUNK_OVERLAP = 150
|
| 33 |
+
|
| 34 |
+
# ๊ฒ์ ์ค์
|
| 35 |
+
TOP_K = 10
|
| 36 |
+
|
| 37 |
+
# ChromaDB ์ค์
|
| 38 |
+
CHROMA_PATH = "./data/chroma_db"
|
| 39 |
+
COLLECTION_NAME = "rfp_documents"
|
| 40 |
+
|
| 41 |
+
# Grok ์ค์
|
| 42 |
+
GROK_MODEL = "grok-3"
|
| 43 |
+
GROK_BASE_URL = "https://api.x.ai/v1"
|
core/__init__.py
ADDED
|
File without changes
|
core/chunker.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/chunker.py
|
| 2 |
+
"""ํ
์คํธ ์ฒญํน"""
|
| 3 |
+
from typing import List, Dict
|
| 4 |
+
from config.settings import CHUNK_SIZE, CHUNK_OVERLAP
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def chunk_text(pages: List[Dict], chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> List[Dict]:
|
| 8 |
+
"""
|
| 9 |
+
ํ
์คํธ๋ฅผ ์ฒญํฌ๋ก ๋ถํ
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
pages: ํ์ด์ง๋ณ ํ
์คํธ
|
| 13 |
+
chunk_size: ์ฒญํฌ ํฌ๊ธฐ
|
| 14 |
+
overlap: ์ค๋ฒ๋ฉ ํฌ๊ธฐ
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
List[Dict]: ์ฒญํฌ ๋ฆฌ์คํธ
|
| 18 |
+
[
|
| 19 |
+
{
|
| 20 |
+
"chunk_id": "chunk_0",
|
| 21 |
+
"text": "...",
|
| 22 |
+
"page_num": 1,
|
| 23 |
+
"start_char": 0,
|
| 24 |
+
"end_char": 800
|
| 25 |
+
},
|
| 26 |
+
...
|
| 27 |
+
]
|
| 28 |
+
"""
|
| 29 |
+
print(f"โ๏ธ ์ฒญํน ์์ (ํฌ๊ธฐ: {chunk_size}, ์ค๋ฒ๋ฉ: {overlap})")
|
| 30 |
+
|
| 31 |
+
chunks = []
|
| 32 |
+
chunk_id = 0
|
| 33 |
+
|
| 34 |
+
for page in pages:
|
| 35 |
+
page_num = page["page_num"]
|
| 36 |
+
text = page["text"]
|
| 37 |
+
|
| 38 |
+
# ํ์ด์ง ํ
์คํธ๋ฅผ ์ฒญํฌ๋ก ๋ถํ
|
| 39 |
+
start = 0
|
| 40 |
+
while start < len(text):
|
| 41 |
+
end = start + chunk_size
|
| 42 |
+
chunk_text = text[start:end]
|
| 43 |
+
|
| 44 |
+
# ๋น ์ฒญํฌ ์ ์ธ
|
| 45 |
+
if chunk_text.strip():
|
| 46 |
+
chunks.append({
|
| 47 |
+
"chunk_id": f"chunk_{chunk_id}",
|
| 48 |
+
"text": chunk_text,
|
| 49 |
+
"page_num": page_num,
|
| 50 |
+
"start_char": start,
|
| 51 |
+
"end_char": end
|
| 52 |
+
})
|
| 53 |
+
chunk_id += 1
|
| 54 |
+
|
| 55 |
+
# ๋ค์ ์ฒญํฌ ์์ ์์น (์ค๋ฒ๋ฉ ๊ณ ๋ ค)
|
| 56 |
+
start += (chunk_size - overlap)
|
| 57 |
+
|
| 58 |
+
print(f"โ
{len(chunks)}๊ฐ ์ฒญํฌ ์์ฑ")
|
| 59 |
+
|
| 60 |
+
return chunks
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
# ํ
์คํธ
|
| 65 |
+
test_pages = [
|
| 66 |
+
{"page_num": 1, "text": "ํ
์คํธ " * 500},
|
| 67 |
+
{"page_num": 2, "text": "๋ฌธ์ " * 500}
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
chunks = chunk_text(test_pages)
|
| 71 |
+
print(f"์์ฑ๋ ์ฒญํฌ ์: {len(chunks)}")
|
| 72 |
+
print(f"์ฒซ ์ฒญํฌ: {chunks[0]}")
|
| 73 |
+
|
core/embedder.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/embedder.py
|
| 2 |
+
"""์๋ฒ ๋ฉ ์์ฑ"""
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
from config.settings import OPENAI_API_KEY, EMBEDDING_MODEL
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def embed_chunks(chunks: List[Dict]) -> List[Dict]:
|
| 12 |
+
"""
|
| 13 |
+
์ฒญํฌ ๋ฆฌ์คํธ๋ฅผ ์๋ฒ ๋ฉ
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
chunks: ์ฒญํฌ ๋ฆฌ์คํธ
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
List[Dict]: ์๋ฒ ๋ฉ์ด ์ถ๊ฐ๋ ์ฒญํฌ
|
| 20 |
+
[
|
| 21 |
+
{
|
| 22 |
+
"chunk_id": "...",
|
| 23 |
+
"text": "...",
|
| 24 |
+
"embedding": [0.1, 0.2, ...],
|
| 25 |
+
...
|
| 26 |
+
},
|
| 27 |
+
...
|
| 28 |
+
]
|
| 29 |
+
"""
|
| 30 |
+
print(f"๐ข ์๋ฒ ๋ฉ ์์ ({len(chunks)}๊ฐ ์ฒญํฌ)")
|
| 31 |
+
start_time = time.time()
|
| 32 |
+
|
| 33 |
+
# ๋ฐฐ์น ์ฒ๋ฆฌ (OpenAI๋ ํ ๋ฒ์ ์ฌ๋ฌ ๊ฐ ๊ฐ๋ฅ)
|
| 34 |
+
texts = [chunk["text"] for chunk in chunks]
|
| 35 |
+
|
| 36 |
+
response = client.embeddings.create(
|
| 37 |
+
model=EMBEDDING_MODEL,
|
| 38 |
+
input=texts
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# ์๋ฒ ๋ฉ ์ถ๊ฐ
|
| 42 |
+
for i, chunk in enumerate(chunks):
|
| 43 |
+
chunk["embedding"] = response.data[i].embedding
|
| 44 |
+
|
| 45 |
+
elapsed = time.time() - start_time
|
| 46 |
+
print(f"โ
์๋ฒ ๋ฉ ์๋ฃ ({elapsed:.2f}์ด)")
|
| 47 |
+
print(f" - ์๋: {len(chunks)/elapsed:.2f} chunks/sec")
|
| 48 |
+
|
| 49 |
+
return chunks
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
# ํ
์คํธ
|
| 54 |
+
test_chunks = [
|
| 55 |
+
{"chunk_id": "chunk_0", "text": "ํ
์คํธ ๋ฌธ์์
๋๋ค."}
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
embedded = embed_chunks(test_chunks)
|
| 59 |
+
print(f"์๋ฒ ๋ฉ ์ฐจ์: {len(embedded[0]['embedding'])}")
|
| 60 |
+
|
core/generator.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/generator.py
|
| 2 |
+
"""Grok ๋ต๋ณ ์์ฑ"""
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
from config.settings import XAI_API_KEY, GROK_MODEL, GROK_BASE_URL
|
| 6 |
+
|
| 7 |
+
# Grok ํด๋ผ์ด์ธํธ ์ด๊ธฐํ
|
| 8 |
+
client = OpenAI(
|
| 9 |
+
api_key=XAI_API_KEY,
|
| 10 |
+
base_url=GROK_BASE_URL
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Generator:
|
| 15 |
+
"""๋ต๋ณ ์์ฑ ํด๋์ค"""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
"""์์ฑ๊ธฐ ์ด๊ธฐํ"""
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
def generate_answer(self, query: str, retrieved_chunks: List[Dict]) -> Dict:
|
| 22 |
+
"""
|
| 23 |
+
๊ฒ์๋ ์ฒญํฌ๋ฅผ ๋ฐํ์ผ๋ก ๋ต๋ณ ์์ฑ
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
query: ์ฌ์ฉ์ ์ง๋ฌธ
|
| 27 |
+
retrieved_chunks: ๊ฒ์๋ ์ฒญํฌ ๋ฆฌ์คํธ
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Dict: {
|
| 31 |
+
"answer": "๋ต๋ณ ํ
์คํธ",
|
| 32 |
+
"sources": [
|
| 33 |
+
{"page_num": 1, "text": "...", "chunk_id": "..."},
|
| 34 |
+
...
|
| 35 |
+
]
|
| 36 |
+
}
|
| 37 |
+
"""
|
| 38 |
+
print(f"๐ค Grok ๋ต๋ณ ์์ฑ ์ค...")
|
| 39 |
+
|
| 40 |
+
# ์ปจํ
์คํธ ๊ตฌ์ฑ
|
| 41 |
+
context = self._build_context(retrieved_chunks)
|
| 42 |
+
|
| 43 |
+
# ํ๋กฌํํธ ๊ตฌ์ฑ
|
| 44 |
+
prompt = self._build_prompt(query, context)
|
| 45 |
+
|
| 46 |
+
# Grok ํธ์ถ
|
| 47 |
+
response = client.chat.completions.create(
|
| 48 |
+
model=GROK_MODEL,
|
| 49 |
+
messages=[
|
| 50 |
+
{
|
| 51 |
+
"role": "system",
|
| 52 |
+
"content": """๋น์ ์ RFP(์ ์์์ฒญ์) ๋ฌธ์ ๋ถ์ ์ ๋ฌธ๊ฐ์
๋๋ค.
|
| 53 |
+
์ ๊ณต๋ ๋ฌธ์ ๋ด์ฉ๋ง์ ๊ธฐ๋ฐ์ผ๋ก ์ ํํ๊ณ ๊ตฌ์ฒด์ ์ผ๋ก ๋ต๋ณํ์ธ์.
|
| 54 |
+
๋ฌธ์์ ์๋ ๋ด์ฉ์ ์ถ์ธกํ์ง ๋ง๊ณ ์์งํ "๋ฌธ์์์ ํด๋น ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค"๋ผ๊ณ ๋ต๋ณํ์ธ์.
|
| 55 |
+
๋ต๋ณ ์ ๋ฐ๋์ ์ถ์ฒ ํ์ด์ง ๋ฒํธ๋ฅผ [ํ์ด์ง X] ํ์์ผ๋ก ๋ช
์ํ์ธ์."""
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"role": "user",
|
| 59 |
+
"content": prompt
|
| 60 |
+
}
|
| 61 |
+
],
|
| 62 |
+
temperature=0.3,
|
| 63 |
+
max_tokens=1000
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
answer = response.choices[0].message.content
|
| 67 |
+
|
| 68 |
+
# ๋ต๋ณ์์ ํ์ด์ง ๋ฒํธ ์ถ์ถ (์: [ํ์ด์ง 7])
|
| 69 |
+
import re
|
| 70 |
+
page_matches = re.findall(r'\[ํ์ด์ง\s*(\d+)\]', answer)
|
| 71 |
+
mentioned_page = int(page_matches[0]) if page_matches else None
|
| 72 |
+
|
| 73 |
+
# ์์ค ์ ๋ฆฌ (๋ต๋ณ์ ์ธ๊ธ๋ ํ์ด์ง ์ฐ์ )
|
| 74 |
+
sources = self._extract_sources(retrieved_chunks, mentioned_page)
|
| 75 |
+
|
| 76 |
+
print(f"โ
๋ต๋ณ ์์ฑ ์๋ฃ ({len(answer)}์)")
|
| 77 |
+
if mentioned_page:
|
| 78 |
+
print(f" ๐ ๋ต๋ณ ์ถ์ฒ: ํ์ด์ง {mentioned_page}")
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
"answer": answer,
|
| 82 |
+
"sources": sources
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
def _build_context(self, chunks: List[Dict]) -> str:
|
| 86 |
+
"""
|
| 87 |
+
์ฒญํฌ๋ฅผ ์ปจํ
์คํธ ๋ฌธ์์ด๋ก ๋ณํ
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
chunks: ์ฒญํฌ ๋ฆฌ์คํธ
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
str: ์ปจํ
์คํธ ๋ฌธ์์ด
|
| 94 |
+
"""
|
| 95 |
+
context_parts = []
|
| 96 |
+
for i, chunk in enumerate(chunks, 1):
|
| 97 |
+
context_parts.append(
|
| 98 |
+
f"[๋ฌธ์ {i} - ํ์ด์ง {chunk['page_num']}]\n{chunk['text']}\n"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
return "\n".join(context_parts)
|
| 102 |
+
|
| 103 |
+
def _build_prompt(self, query: str, context: str) -> str:
|
| 104 |
+
"""
|
| 105 |
+
ํ๋กฌํํธ ๊ตฌ์ฑ (ํ์ด๋ผ์ดํธ์ฉ ์ธ์ฉ๋ฌธ ํฌํจ)
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
query: ์ฌ์ฉ์ ์ง๋ฌธ
|
| 109 |
+
context: ์ปจํ
์คํธ
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
str: ํ๋กฌํํธ
|
| 113 |
+
"""
|
| 114 |
+
prompt = f"""๋น์ ์ RFP(์ ์์์ฒญ์) ๋ฌธ์ ๋ถ์ ์ ๋ฌธ๊ฐ์
๋๋ค.
|
| 115 |
+
๋ค์ ๋ฌธ์ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ง๋ฌธ์ ๋ต๋ณํ์ธ์.
|
| 116 |
+
|
| 117 |
+
# ๋ฌธ์ ๋ด์ฉ
|
| 118 |
+
{context}
|
| 119 |
+
|
| 120 |
+
# ์ง๋ฌธ
|
| 121 |
+
{query}
|
| 122 |
+
|
| 123 |
+
# ๋ต๋ณ ๊ท์น
|
| 124 |
+
1. ๋ฐ๋์ ์ ๊ณต๋ ๋ฌธ์ ๋ด์ฉ๋ง์ ๊ธฐ๋ฐ์ผ๋ก ๋ต๋ณํ์ธ์
|
| 125 |
+
2. ๋ฌธ์์ ์๋ ๋ด์ฉ์ด๋ฉด "์ ๊ณต๋ ๋ฌธ์์์ ํด๋น ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค"๋ผ๊ณ ๋ต๋ณํ์ธ์
|
| 126 |
+
3. ๋ต๋ณ ์ ์ถ์ฒ ํ์ด์ง ๋ฒํธ๋ฅผ [ํ์ด์ง X] ํ์์ผ๋ก ๋ช
์ํ์ธ์
|
| 127 |
+
4. **์ค์**: ๋ต๋ณ์ ํต์ฌ ๊ทผ๊ฑฐ๊ฐ ๋๋ ๋ฌธ์์ ์๋ฌธ์ ๊ทธ๋๋ก ์ธ์ฉํ์ธ์
|
| 128 |
+
- ๋ต๋ณ ๋์ "---ํต์ฌ ์ธ์ฉ---" ๋ค์์ PDF์์ ํ์ผ๋ผ์ดํธํ ์ ํํ ๋ฌธ์ฅ์ ์ ์ด์ฃผ์ธ์
|
| 129 |
+
- ์ธ์ฉ๋ฌธ์ ๋ฌธ์ ์๋ฌธ ๊ทธ๋๋ก, 10-30๋จ์ด ์ ๋๋ก ์ ์ด์ฃผ์ธ์
|
| 130 |
+
5. ๋ช
ํํ๊ณ ๊ฐ๊ฒฐํ๊ฒ ๋ต๋ณํ์ธ์
|
| 131 |
+
|
| 132 |
+
# ๋ต๋ณ ํ์ ์์
|
| 133 |
+
(๋ต๋ณ ๋ด์ฉ)... [ํ์ด์ง 7]
|
| 134 |
+
|
| 135 |
+
---ํต์ฌ ์ธ์ฉ---
|
| 136 |
+
์ฐจ์ฒด ๋ฐ ์ผ๋ฐ๋ถํ์ ๋ณด์ฆ๊ธฐ๊ฐ: 3๋
/6๋งkm ์ด๋ด
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
return prompt
|
| 140 |
+
|
| 141 |
+
def _extract_sources(self, chunks: List[Dict], mentioned_page: int = None) -> List[Dict]:
|
| 142 |
+
"""
|
| 143 |
+
์์ค ์ ๋ณด ์ถ์ถ (๋ต๋ณ์ ์ธ๊ธ๋ ํ์ด์ง ์ฐ์ )
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
chunks: ์ฒญํฌ ๋ฆฌ์คํธ (์ด๋ฏธ ๊ด๋ จ๋ ์์ผ๋ก ์ ๋ ฌ๋จ)
|
| 147 |
+
mentioned_page: AI ๋ต๋ณ์ ์ธ๊ธ๋ ํ์ด์ง ๋ฒํธ
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
List[Dict]: ์์ค ์ ๋ณด (mentioned_page๊ฐ ์๏ฟฝ๏ฟฝ๋ฉด ํด๋น ํ์ด์ง ์ฒญํฌ๋ฅผ 1์๋ก)
|
| 151 |
+
"""
|
| 152 |
+
# mentioned_page๊ฐ ์์ผ๋ฉด ํด๋น ํ์ด์ง ์ฒญํฌ๋ฅผ ์ต์ฐ์ ์ผ๋ก
|
| 153 |
+
if mentioned_page:
|
| 154 |
+
# ํด๋น ํ์ด์ง์ ์ฒญํฌ๋ฅผ ์ฐพ์์ ๋งจ ์์ผ๋ก
|
| 155 |
+
reordered_chunks = []
|
| 156 |
+
mentioned_chunks = [c for c in chunks if c["page_num"] == mentioned_page]
|
| 157 |
+
other_chunks = [c for c in chunks if c["page_num"] != mentioned_page]
|
| 158 |
+
|
| 159 |
+
reordered_chunks = mentioned_chunks + other_chunks
|
| 160 |
+
chunks = reordered_chunks
|
| 161 |
+
|
| 162 |
+
sources = []
|
| 163 |
+
seen_pages = set()
|
| 164 |
+
|
| 165 |
+
for chunk in chunks[:5]: # ์์ 5๊ฐ๋ง
|
| 166 |
+
page_num = chunk["page_num"]
|
| 167 |
+
if page_num not in seen_pages:
|
| 168 |
+
sources.append({
|
| 169 |
+
"page_num": page_num,
|
| 170 |
+
"text": chunk["text"][:200] + "...", # ๋ฏธ๋ฆฌ๋ณด๊ธฐ
|
| 171 |
+
"chunk_id": chunk["chunk_id"]
|
| 172 |
+
})
|
| 173 |
+
seen_pages.add(page_num)
|
| 174 |
+
|
| 175 |
+
return sources
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
# ํ
์คํธ
|
| 180 |
+
generator = Generator()
|
| 181 |
+
|
| 182 |
+
test_chunks = [
|
| 183 |
+
{
|
| 184 |
+
"chunk_id": "chunk_0",
|
| 185 |
+
"text": "์ด ํ๋ก์ ํธ์ ์์ฐ์ 1์ต์์
๋๋ค.",
|
| 186 |
+
"page_num": 1
|
| 187 |
+
}
|
| 188 |
+
]
|
| 189 |
+
|
| 190 |
+
result = generator.generate_answer("์์ฐ์ด ์ผ๋ง์ธ๊ฐ์?", test_chunks)
|
| 191 |
+
print(f"๋ต๋ณ: {result['answer']}")
|
| 192 |
+
print(f"์ถ์ฒ: {len(result['sources'])}๊ฐ")
|
| 193 |
+
|
core/pdf_loader.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/pdf_loader.py
|
| 2 |
+
"""PDF ํ
์คํธ ์ถ์ถ"""
|
| 3 |
+
import pymupdf4llm
|
| 4 |
+
import fitz # PyMuPDF
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load_pdf(pdf_path: str) -> Dict:
|
| 9 |
+
"""
|
| 10 |
+
PDF ํ์ผ ๋ก๋ ๋ฐ ํ
์คํธ ์ถ์ถ
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
pdf_path: PDF ํ์ผ ๊ฒฝ๋ก
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
Dict: {
|
| 17 |
+
"text": "์ ์ฒด ํ
์คํธ",
|
| 18 |
+
"pages": [
|
| 19 |
+
{"page_num": 1, "text": "..."},
|
| 20 |
+
...
|
| 21 |
+
]
|
| 22 |
+
}
|
| 23 |
+
"""
|
| 24 |
+
print(f"๐ PDF ๋ก๋ ์ค: {pdf_path}")
|
| 25 |
+
|
| 26 |
+
# pymupdf4llm์ผ๋ก ํ
์คํธ ์ถ์ถ (markdown ํ์)
|
| 27 |
+
md_text = pymupdf4llm.to_markdown(pdf_path)
|
| 28 |
+
|
| 29 |
+
# ํ์ด์ง๋ณ๋ก ๋ถ๋ฆฌ
|
| 30 |
+
doc = fitz.open(pdf_path)
|
| 31 |
+
|
| 32 |
+
pages = []
|
| 33 |
+
for page_num, page in enumerate(doc, start=1):
|
| 34 |
+
page_text = page.get_text()
|
| 35 |
+
pages.append({
|
| 36 |
+
"page_num": page_num,
|
| 37 |
+
"text": page_text
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
doc.close()
|
| 41 |
+
|
| 42 |
+
print(f"โ
{len(pages)}ํ์ด์ง ์ถ์ถ ์๋ฃ")
|
| 43 |
+
|
| 44 |
+
return {
|
| 45 |
+
"text": md_text,
|
| 46 |
+
"pages": pages,
|
| 47 |
+
"total_pages": len(pages)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
# ํ
์คํธ
|
| 53 |
+
import sys
|
| 54 |
+
if len(sys.argv) > 1:
|
| 55 |
+
result = load_pdf(sys.argv[1])
|
| 56 |
+
print(f"์ด ํ์ด์ง: {result['total_pages']}")
|
| 57 |
+
print(f"์ฒซ ํ์ด์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ: {result['pages'][0]['text'][:200]}")
|
| 58 |
+
|
core/retriever.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/retriever.py
|
| 2 |
+
"""๊ฒ์ ๋ก์ง - ๊ด๋ จ๋ ์ ์ ํฌํจ"""
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
from config.settings import OPENAI_API_KEY, EMBEDDING_MODEL, TOP_K
|
| 6 |
+
|
| 7 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Retriever:
|
| 11 |
+
"""RAG ๊ฒ์ ํด๋์ค (1D ์ ์ฉ ๋ฒ์ )"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, vectordb):
|
| 14 |
+
self.vectordb = vectordb
|
| 15 |
+
|
| 16 |
+
def retrieve(self, query: str, top_k: int = TOP_K) -> List[Dict]:
|
| 17 |
+
"""
|
| 18 |
+
์ง๋ฌธ(query)์ ๊ฐ์ฅ ์ ์ฌํ ์ฒญํฌ๋ฅผ ๋ฒกํฐDB์์ ๊ฒ์ํ์ฌ
|
| 19 |
+
๊ด๋ จ๋ ์ ์(relevance_score)๊น์ง ๊ณ์ฐํ ํ ๋ฐํ.
|
| 20 |
+
|
| 21 |
+
**์ค์**: distance๊ฐ ์์์๋ก ๊ด๋ จ๋๊ฐ ๋์ โ ์ค๋ฆ์ฐจ์ ์ ๋ ฌ
|
| 22 |
+
"""
|
| 23 |
+
print(f"๐ ๊ฒ์ ์ค: '{query[:50]}...'")
|
| 24 |
+
|
| 25 |
+
# --- 1. ์ง๋ฌธ ์๋ฒ ๋ฉ ์์ฑ ---
|
| 26 |
+
embedding_response = client.embeddings.create(
|
| 27 |
+
model=EMBEDDING_MODEL,
|
| 28 |
+
input=[query]
|
| 29 |
+
)
|
| 30 |
+
query_embedding = embedding_response.data[0].embedding
|
| 31 |
+
|
| 32 |
+
# --- 2. ๋ฒกํฐ ๊ฒ์ (1D ๊ตฌ์กฐ) ---
|
| 33 |
+
results = self.vectordb.search(query_embedding, top_k)
|
| 34 |
+
|
| 35 |
+
ids = results["ids"]
|
| 36 |
+
docs = results["documents"]
|
| 37 |
+
metas = results["metadatas"]
|
| 38 |
+
dists = results["distances"]
|
| 39 |
+
|
| 40 |
+
chunks = []
|
| 41 |
+
|
| 42 |
+
# --- 3. ํฌ๋งทํ
+ ๊ด๋ จ๋ ์ ์ ๊ณ์ฐ ---
|
| 43 |
+
for i in range(len(ids)):
|
| 44 |
+
distance = dists[i] if i < len(dists) else 1.0
|
| 45 |
+
relevance_score = round(1.0 - distance, 4)
|
| 46 |
+
|
| 47 |
+
meta = metas[i] if isinstance(metas[i], dict) else {}
|
| 48 |
+
|
| 49 |
+
chunks.append({
|
| 50 |
+
"chunk_id": ids[i],
|
| 51 |
+
"text": docs[i],
|
| 52 |
+
"page_num": meta.get("page_num", 0),
|
| 53 |
+
"start_char": meta.get("start_char", 0),
|
| 54 |
+
"end_char": meta.get("end_char", 0),
|
| 55 |
+
"distance": distance,
|
| 56 |
+
"relevance_score": relevance_score
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
# --- 4. โ
distance ์ค๋ฆ์ฐจ์ ์ ๋ ฌ (์์์๋ก ๊ด๋ จ๋ ๋์) ---
|
| 60 |
+
chunks.sort(key=lambda x: x["distance"])
|
| 61 |
+
|
| 62 |
+
# ๋๋ฒ๊ทธ: ์์ 3๊ฐ ์ถ๋ ฅ
|
| 63 |
+
print(f"โ
{len(chunks)}๊ฐ ์ฒญํฌ ๊ฒ์ ์๋ฃ")
|
| 64 |
+
if chunks:
|
| 65 |
+
print(f" ๐ฏ ์ต๊ณ ๊ด๋ จ๋: p.{chunks[0]['page_num']} (distance: {chunks[0]['distance']:.4f})")
|
| 66 |
+
|
| 67 |
+
return chunks
|
core/vectordb.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/vectordb.py
|
| 2 |
+
"""ChromaDB ๊ด๋ฆฌ (ํ๋ก๋์
๋ฒ์ )"""
|
| 3 |
+
import chromadb
|
| 4 |
+
import uuid
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import List, Dict
|
| 7 |
+
from config.settings import CHROMA_PATH
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class VectorDB:
|
| 11 |
+
"""ChromaDB ๊ด๋ฆฌ ํด๋์ค (ํ๋ก๋์
์์ ํ)"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, persist_directory: str = CHROMA_PATH, session_id: str = None):
|
| 14 |
+
"""
|
| 15 |
+
ChromaDB ์ด๊ธฐํ (์ธ์
๋ณ ์ปฌ๋ ์
)
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
persist_directory: ์ ์ฅ ๊ฒฝ๋ก
|
| 19 |
+
session_id: ์ธ์
ID (์์ผ๋ฉด ์๋ ์์ฑ)
|
| 20 |
+
"""
|
| 21 |
+
print(f"๐๏ธ ChromaDB ์ด๊ธฐํ ์ค...")
|
| 22 |
+
|
| 23 |
+
self.persist_directory = persist_directory
|
| 24 |
+
|
| 25 |
+
# ์ธ์
ID ๊ธฐ๋ฐ ์ปฌ๋ ์
์ด๋ฆ
|
| 26 |
+
if session_id is None:
|
| 27 |
+
session_id = str(uuid.uuid4())[:8]
|
| 28 |
+
|
| 29 |
+
self.collection_name = f"session_{session_id}"
|
| 30 |
+
print(f" ๐ ์ปฌ๋ ์
์ด๋ฆ: {self.collection_name}")
|
| 31 |
+
|
| 32 |
+
# PersistentClient ์์ฑ
|
| 33 |
+
self.client = chromadb.PersistentClient(path=persist_directory)
|
| 34 |
+
|
| 35 |
+
# โ
get_or_create ์ฌ์ฉ (ํ๋ก๋์
ํ์ค)
|
| 36 |
+
self.collection = self.client.get_or_create_collection(
|
| 37 |
+
name=self.collection_name,
|
| 38 |
+
metadata={"description": "RFP ๋ฌธ์ ์๋ฒ ๋ฉ"}
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
print(f"โ
ChromaDB ์ค๋น ์๋ฃ (ํ์ฌ {self.collection.count()}๊ฐ ์ฒญํฌ)")
|
| 42 |
+
|
| 43 |
+
def add_chunks(self, chunks: List[Dict]):
|
| 44 |
+
"""์ฒญํฌ ์ ์ฅ"""
|
| 45 |
+
print(f"๐พ {len(chunks)}๊ฐ ์ฒญํฌ ์ ์ฅ ์ค...")
|
| 46 |
+
|
| 47 |
+
ids = [chunk["chunk_id"] for chunk in chunks]
|
| 48 |
+
embeddings = [chunk["embedding"] for chunk in chunks]
|
| 49 |
+
documents = [chunk["text"] for chunk in chunks]
|
| 50 |
+
|
| 51 |
+
metadatas = []
|
| 52 |
+
for chunk in chunks:
|
| 53 |
+
metadatas.append({
|
| 54 |
+
"page_num": chunk.get("page_num", 0),
|
| 55 |
+
"start_char": chunk.get("start_char", 0),
|
| 56 |
+
"end_char": chunk.get("end_char", 0)
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
self.collection.add(
|
| 60 |
+
ids=ids,
|
| 61 |
+
embeddings=embeddings,
|
| 62 |
+
documents=documents,
|
| 63 |
+
metadatas=metadatas
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
print("โ
์ ์ฅ ์๋ฃ")
|
| 67 |
+
|
| 68 |
+
def search(self, query_embedding: List[float], top_k: int = 10) -> Dict:
|
| 69 |
+
"""๋ฒกํฐ ๊ฒ์"""
|
| 70 |
+
results = self.collection.query(
|
| 71 |
+
query_embeddings=[query_embedding],
|
| 72 |
+
n_results=top_k
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
"ids": results["ids"][0],
|
| 77 |
+
"documents": results["documents"][0],
|
| 78 |
+
"metadatas": results["metadatas"][0],
|
| 79 |
+
"distances": results["distances"][0]
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
def delete_collection(self):
|
| 83 |
+
"""ํ์ฌ ์ปฌ๋ ์
์ญ์ """
|
| 84 |
+
print(f"๐๏ธ ์ปฌ๋ ์
์ญ์ ์ค: {self.collection_name}")
|
| 85 |
+
try:
|
| 86 |
+
self.client.delete_collection(name=self.collection_name)
|
| 87 |
+
print(" โ
์ญ์ ์๋ฃ")
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f" โ ๏ธ ์ญ์ ์ค๋ฅ: {e}")
|
| 90 |
+
|
| 91 |
+
def count(self) -> int:
|
| 92 |
+
"""ํ์ฌ ์ฒญํฌ ๊ฐ์"""
|
| 93 |
+
return self.collection.count()
|
| 94 |
+
|
| 95 |
+
def get_stats(self) -> Dict:
|
| 96 |
+
"""ํต๊ณ"""
|
| 97 |
+
return {
|
| 98 |
+
"total_chunks": self.collection.count(),
|
| 99 |
+
"collection_name": self.collection_name
|
| 100 |
+
}
|
ui/__init__.py
ADDED
|
File without changes
|
ui/components.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ui/components.py
|
| 2 |
+
"""PROBIN UI ์ปดํฌ๋ํธ"""
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def render_header():
|
| 8 |
+
"""ํค๋ ๋ ๋๋ง (PROBIN ๋ฒ์ )"""
|
| 9 |
+
st.markdown(
|
| 10 |
+
"""
|
| 11 |
+
<div style="text-align: center; padding: 2rem 0;">
|
| 12 |
+
<h1 style="color: #2196F3; font-size: 3rem;">๐ง PROBIN</h1>
|
| 13 |
+
<p style="font-size: 1.2rem; color: #666;">Intelligent Document Analysis System</p>
|
| 14 |
+
<p style="font-size: 0.9rem; color: #999;">์ ํ๋ ์ฐ์ RAG ์์คํ
</p>
|
| 15 |
+
</div>
|
| 16 |
+
""",
|
| 17 |
+
unsafe_allow_html=True
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def render_sources_with_relevance(sources: List[Dict], message_idx: int, move_to_page_callback):
|
| 22 |
+
"""
|
| 23 |
+
์ถ์ฒ ๋ ๋๋ง (๊ฐ์ฅ ๊ด๋ จ๋ ๋์ ๊ฒ ์ฐ์ )
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
sources: ์ถ์ฒ ๋ฆฌ์คํธ (์ด๋ฏธ ๊ด๋ จ๋ ์์ผ๋ก ์ ๋ ฌ๋จ)
|
| 27 |
+
message_idx: ๋ฉ์์ง ์ธ๋ฑ์ค (ํค ์ค๋ณต ๋ฐฉ์ง์ฉ)
|
| 28 |
+
move_to_page_callback: ํ์ด์ง ์ด๋ ์ฝ๋ฐฑ ํจ์
|
| 29 |
+
|
| 30 |
+
๊ตฌ์กฐ:
|
| 31 |
+
๐ฏ ํต์ฌ ๊ทผ๊ฑฐ (1๊ฐ) - sources[0]: ๊ฐ์ฅ ๊ด๋ จ๋ ๋์ ์ถ์ฒ
|
| 32 |
+
๐ ์ถ๊ฐ ์ฐธ๊ณ ๋ฌธ์ (๋๋จธ์ง) - Expander ๋ด๋ถ
|
| 33 |
+
"""
|
| 34 |
+
if not sources:
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
# ์ฒซ ๋ฒ์งธ ์ถ์ฒ = ๊ฐ์ฅ ๊ด๋ จ๋ ๋์
|
| 38 |
+
st.markdown("---")
|
| 39 |
+
st.markdown("**๐ฏ ํต์ฌ ๊ทผ๊ฑฐ:**")
|
| 40 |
+
|
| 41 |
+
primary_source = sources[0]
|
| 42 |
+
|
| 43 |
+
if st.button(
|
| 44 |
+
f"๐ ํ์ด์ง {primary_source['page_num']} ํ์ธ",
|
| 45 |
+
key=f"primary_{message_idx}",
|
| 46 |
+
type="primary",
|
| 47 |
+
use_container_width=True
|
| 48 |
+
):
|
| 49 |
+
move_to_page_callback(primary_source['page_num'], primary_source['text'])
|
| 50 |
+
|
| 51 |
+
# ๋ฏธ๋ฆฌ๋ณด๊ธฐ
|
| 52 |
+
st.caption(f"๐ฌ \"{primary_source['text'][:100]}...\"")
|
| 53 |
+
|
| 54 |
+
# ์ถ๊ฐ ์ฐธ๊ณ ๋ฌธ์ (๋๋จธ์ง)
|
| 55 |
+
if len(sources) > 1:
|
| 56 |
+
additional_sources = sources[1:]
|
| 57 |
+
|
| 58 |
+
with st.expander(f"๐ ์ถ๊ฐ ์ฐธ๊ณ ๋ฌธ์ ({min(2, len(additional_sources))}๊ฐ)"):
|
| 59 |
+
cols = st.columns(2)
|
| 60 |
+
|
| 61 |
+
for i, src in enumerate(additional_sources[:2]): # ์ต๋ 2๊ฐ๋ง
|
| 62 |
+
with cols[i]:
|
| 63 |
+
if st.button(
|
| 64 |
+
f"p.{src['page_num']}",
|
| 65 |
+
key=f"additional_{message_idx}_{i}",
|
| 66 |
+
use_container_width=True
|
| 67 |
+
):
|
| 68 |
+
move_to_page_callback(src['page_num'], src['text'])
|
| 69 |
+
|
| 70 |
+
st.caption(f"\"{src['text'][:60]}...\"")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def render_answer(answer: str, sources: List[Dict]):
|
| 75 |
+
"""๋ต๋ณ ๋ฐ ์ถ์ฒ ๋ ๋๋ง (Legacy - ์ฌ์ฉ ์ ํจ)"""
|
| 76 |
+
# ๋ต๋ณ
|
| 77 |
+
st.markdown("### ๐ก ๋ต๋ณ")
|
| 78 |
+
st.markdown(
|
| 79 |
+
f"""
|
| 80 |
+
<div class="answer-box">
|
| 81 |
+
{answer}
|
| 82 |
+
</div>
|
| 83 |
+
""",
|
| 84 |
+
unsafe_allow_html=True
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# ์ถ์ฒ
|
| 88 |
+
if sources:
|
| 89 |
+
st.markdown("### ๐ ์ถ์ฒ")
|
| 90 |
+
for i, source in enumerate(sources, 1):
|
| 91 |
+
with st.expander(f"์ถ์ฒ {i} - ํ์ด์ง {source['page_num']}"):
|
| 92 |
+
st.text(source['text'])
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def render_file_uploader():
|
| 96 |
+
"""ํ์ผ ์
๋ก๋ ๋ ๋๋ง"""
|
| 97 |
+
st.markdown("### ๐ค PDF ์
๋ก๋")
|
| 98 |
+
|
| 99 |
+
uploaded_file = st.file_uploader(
|
| 100 |
+
"RFP PDF ํ์ผ์ ์
๋ก๋ํ์ธ์",
|
| 101 |
+
type=["pdf"],
|
| 102 |
+
help="PDF ํ์์ RFP ๋ฌธ์๋ฅผ ์
๋ก๋ํ์ธ์"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
return uploaded_file
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def render_query_input():
|
| 109 |
+
"""์ง๋ฌธ ์
๋ ฅ ๋ ๋๋ง (Deprecated: st.chat_input ์ฌ์ฉ ๊ถ์ฅ)"""
|
| 110 |
+
st.markdown("### ๐ฌ ์ง๋ฌธํ๊ธฐ")
|
| 111 |
+
|
| 112 |
+
query = st.text_input(
|
| 113 |
+
"์ง๋ฌธ์ ์
๋ ฅํ์ธ์",
|
| 114 |
+
placeholder="์: ์ด ํ๋ก์ ํธ์ ์์ฐ์ ์ผ๋ง์ธ๊ฐ์?",
|
| 115 |
+
help="RFP ๋ฌธ์์ ๋ํด ์ง๋ฌธํ์ธ์"
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
return query
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def render_chat_history(messages: list):
|
| 122 |
+
"""์ฑํ
ํ์คํ ๋ฆฌ ๋ ๋๋ง (ํ๋์ ๋ฐฉ์)"""
|
| 123 |
+
for message in messages:
|
| 124 |
+
with st.chat_message(message["role"]):
|
| 125 |
+
st.markdown(message["content"])
|
| 126 |
+
|
| 127 |
+
# ์ถ์ฒ ํ์
|
| 128 |
+
if message["role"] == "assistant" and "sources" in message:
|
| 129 |
+
with st.expander("๐ ์ถ์ฒ ๋ณด๊ธฐ"):
|
| 130 |
+
for i, src in enumerate(message["sources"], 1):
|
| 131 |
+
st.markdown(f"**์ถ์ฒ {i} - [ํ์ด์ง {src['page_num']}]**")
|
| 132 |
+
st.caption(src["text"])
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def render_sidebar():
|
| 136 |
+
"""์ฌ์ด๋๋ฐ ๋ ๋๋ง"""
|
| 137 |
+
with st.sidebar:
|
| 138 |
+
st.markdown("## โ๏ธ ์ค์ ")
|
| 139 |
+
|
| 140 |
+
# ๊ฒ์ ์ค์
|
| 141 |
+
st.markdown("### ๐ ๊ฒ์ ์ค์ ")
|
| 142 |
+
top_k = st.slider("๊ฒ์ํ ์ฒญํฌ ์", 5, 20, 10)
|
| 143 |
+
|
| 144 |
+
# ์ฒญํน ์ค์
|
| 145 |
+
st.markdown("### โ๏ธ ์ฒญํน ์ค์ ")
|
| 146 |
+
chunk_size = st.number_input("์ฒญํฌ ํฌ๊ธฐ", 400, 1200, 800, step=100)
|
| 147 |
+
chunk_overlap = st.number_input("์ค๋ฒ๋ฉ ํฌ๊ธฐ", 50, 300, 150, step=50)
|
| 148 |
+
|
| 149 |
+
st.markdown("---")
|
| 150 |
+
|
| 151 |
+
# ์ ๋ณด
|
| 152 |
+
st.markdown("### โน๏ธ ์ ๋ณด")
|
| 153 |
+
st.info(
|
| 154 |
+
"""
|
| 155 |
+
**PROBIN v2.0**
|
| 156 |
+
|
| 157 |
+
- PDF ์
๋ก๋ โ
|
| 158 |
+
- ์ง๋ฌธ-๋ต๋ณ โ
|
| 159 |
+
- ์ถ์ฒ ํ์ โ
|
| 160 |
+
- ๋ฒกํฐ ๊ฒ์ โ
|
| 161 |
+
- ํ์ด๋ผ์ดํธ โ
|
| 162 |
+
"""
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# ์ด๊ธฐํ ๋ฒํผ
|
| 166 |
+
if st.button("๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ", type="secondary"):
|
| 167 |
+
return True, top_k, chunk_size, chunk_overlap
|
| 168 |
+
|
| 169 |
+
return False, top_k, chunk_size, chunk_overlap
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def render_processing_status(status: str):
|
| 173 |
+
"""์ฒ๋ฆฌ ์ํ ๋ ๋๋ง"""
|
| 174 |
+
status_icons = {
|
| 175 |
+
"uploading": "๐ค",
|
| 176 |
+
"extracting": "๐",
|
| 177 |
+
"chunking": "โ๏ธ",
|
| 178 |
+
"embedding": "๐ข",
|
| 179 |
+
"storing": "๐พ",
|
| 180 |
+
"complete": "โ
"
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
icon = status_icons.get(status, "โณ")
|
| 184 |
+
st.info(f"{icon} {status}")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def render_welcome_message():
|
| 188 |
+
"""์ฐ์ปด ๋ฉ์์ง (์ฌ์ฉ ์๋ด)"""
|
| 189 |
+
st.markdown("""
|
| 190 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px; text-align: center;">
|
| 191 |
+
<h2 style="margin: 0 0 10px 0;">๐ง PROBIN์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค!</h2>
|
| 192 |
+
<p style="margin: 0; opacity: 0.9;">Intelligent Document Analysis System</p>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<div style="background-color: #f0f7ff; border-left: 4px solid #2196F3; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
|
| 196 |
+
<h4 style="margin-top: 0; color: #1976D2;">๐ ์ด์ฉ ๋ฐฉ๋ฒ</h4>
|
| 197 |
+
<ol style="line-height: 1.8; margin: 0;">
|
| 198 |
+
<li><strong>PDF ์
๋ก๋:</strong> ์ผ์ชฝ ์ฌ์ด๋๋ฐ์์ ๋ฌธ์๋ฅผ ์
๋ก๋ํ์ธ์</li>
|
| 199 |
+
<li><strong>๋ฌธ์ ์ฒ๋ฆฌ:</strong> 30์ด~1๋ถ ์ ๋ ๊ธฐ๋ค๋ฆฝ๋๋ค</li>
|
| 200 |
+
<li><strong>์ง๋ฌธ ์
๋ ฅ:</strong> ์ฑํ
์ฐฝ์ ์ง๋ฌธ์ ์
๋ ฅํ์ธ์</li>
|
| 201 |
+
<li><strong>๊ทผ๊ฑฐ ํ์ธ:</strong> <span style="background-color: rgba(255,255,0,0.5); padding: 2px 6px; border-radius: 3px;">๋
ธ๋์ ํ์ด๋ผ์ดํธ</span>๋ก ๊ทผ๊ฑฐ๋ฅผ ํ์ธํ์ธ์</li>
|
| 202 |
+
</ol>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div style="text-align: center; margin-top: 20px;">
|
| 206 |
+
<span style="display: inline-block; margin: 5px; padding: 8px 16px; background-color: #e8f5e9; border-radius: 20px; font-size: 0.9rem;">๐บ Split View</span>
|
| 207 |
+
<span style="display: inline-block; margin: 5px; padding: 8px 16px; background-color: #fff3e0; border-radius: 20px; font-size: 0.9rem;">๐๏ธ Highlighting</span>
|
| 208 |
+
<span style="display: inline-block; margin: 5px; padding: 8px 16px; background-color: #f3e5f5; border-radius: 20px; font-size: 0.9rem;">๐ฏ High Accuracy</span>
|
| 209 |
+
</div>
|
| 210 |
+
""", unsafe_allow_html=True)
|
ui/styles.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ui/styles.py
|
| 2 |
+
"""UI ์คํ์ผ ์ ์ - Claude ์คํ์ผ ๋คํฌ ๋ฐฐ๊ฒฝ"""
|
| 3 |
+
|
| 4 |
+
def get_custom_css():
|
| 5 |
+
"""์ปค์คํ
CSS ๋ฐํ"""
|
| 6 |
+
return """
|
| 7 |
+
<style>
|
| 8 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 9 |
+
๐ ์ ๋๋ฉ์ด์
๋ฐฐ๊ฒฝ (Pure CSS)
|
| 10 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 11 |
+
.animated-background {
|
| 12 |
+
position: fixed;
|
| 13 |
+
width: 100%;
|
| 14 |
+
height: 100%;
|
| 15 |
+
top: 0;
|
| 16 |
+
left: 0;
|
| 17 |
+
z-index: -1;
|
| 18 |
+
background: linear-gradient(
|
| 19 |
+
45deg,
|
| 20 |
+
#667eea 0%,
|
| 21 |
+
#764ba2 25%,
|
| 22 |
+
#f093fb 50%,
|
| 23 |
+
#4facfe 75%,
|
| 24 |
+
#667eea 100%
|
| 25 |
+
);
|
| 26 |
+
background-size: 400% 400%;
|
| 27 |
+
animation: gradientFlow 15s ease infinite;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* ๋ฐฐ๊ฒฝ ์์ ๋ ๋ค๋๋ ์๋ค */
|
| 31 |
+
.animated-background::before,
|
| 32 |
+
.animated-background::after {
|
| 33 |
+
content: '';
|
| 34 |
+
position: absolute;
|
| 35 |
+
border-radius: 50%;
|
| 36 |
+
filter: blur(80px);
|
| 37 |
+
opacity: 0.2;
|
| 38 |
+
animation: float 25s ease-in-out infinite;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.animated-background::before {
|
| 42 |
+
width: 600px;
|
| 43 |
+
height: 600px;
|
| 44 |
+
background: radial-gradient(circle, rgba(255,255,255,0.2), transparent);
|
| 45 |
+
top: 10%;
|
| 46 |
+
left: 20%;
|
| 47 |
+
animation-delay: 0s;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.animated-background::after {
|
| 51 |
+
width: 500px;
|
| 52 |
+
height: 500px;
|
| 53 |
+
background: radial-gradient(circle, rgba(255,255,255,0.15), transparent);
|
| 54 |
+
bottom: 15%;
|
| 55 |
+
right: 10%;
|
| 56 |
+
animation-delay: 7s;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
@keyframes gradientFlow {
|
| 60 |
+
0% { background-position: 0% 50%; }
|
| 61 |
+
50% { background-position: 100% 50%; }
|
| 62 |
+
100% { background-position: 0% 50%; }
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
@keyframes float {
|
| 66 |
+
0%, 100% { transform: translate(0, 0) scale(1); }
|
| 67 |
+
25% { transform: translate(40px, -40px) scale(1.1); }
|
| 68 |
+
50% { transform: translate(-30px, 30px) scale(0.9); }
|
| 69 |
+
75% { transform: translate(50px, 15px) scale(1.05); }
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 73 |
+
๐ Hero Container (์ด๊ธฐ ํ๋ฉด)
|
| 74 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 75 |
+
.hero-container {
|
| 76 |
+
position: relative;
|
| 77 |
+
text-align: center;
|
| 78 |
+
padding: 10rem 2rem;
|
| 79 |
+
min-height: 80vh;
|
| 80 |
+
display: flex;
|
| 81 |
+
flex-direction: column;
|
| 82 |
+
justify-content: center;
|
| 83 |
+
align-items: center;
|
| 84 |
+
background: transparent;
|
| 85 |
+
z-index: 1;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.hero-title {
|
| 89 |
+
font-size: 5rem;
|
| 90 |
+
font-weight: 900;
|
| 91 |
+
color: white;
|
| 92 |
+
margin-bottom: 1.5rem;
|
| 93 |
+
text-shadow:
|
| 94 |
+
0 0 30px rgba(255,255,255,0.6),
|
| 95 |
+
0 0 50px rgba(102,126,234,0.4),
|
| 96 |
+
3px 3px 40px rgba(0,0,0,0.4);
|
| 97 |
+
animation: titlePulse 4s ease-in-out infinite;
|
| 98 |
+
letter-spacing: 4px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
@keyframes titlePulse {
|
| 102 |
+
0%, 100% {
|
| 103 |
+
transform: scale(1);
|
| 104 |
+
text-shadow:
|
| 105 |
+
0 0 30px rgba(255,255,255,0.6),
|
| 106 |
+
0 0 50px rgba(102,126,234,0.4);
|
| 107 |
+
}
|
| 108 |
+
50% {
|
| 109 |
+
transform: scale(1.03);
|
| 110 |
+
text-shadow:
|
| 111 |
+
0 0 40px rgba(255,255,255,0.8),
|
| 112 |
+
0 0 70px rgba(102,126,234,0.6),
|
| 113 |
+
0 0 100px rgba(118,75,162,0.4);
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.hero-subtitle {
|
| 118 |
+
font-size: 1.9rem;
|
| 119 |
+
color: rgba(255,255,255,0.95);
|
| 120 |
+
font-weight: 300;
|
| 121 |
+
letter-spacing: 4px;
|
| 122 |
+
text-shadow:
|
| 123 |
+
0 0 15px rgba(255,255,255,0.4),
|
| 124 |
+
2px 2px 25px rgba(0,0,0,0.3);
|
| 125 |
+
animation: subtitleFade 2.5s ease-out;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
@keyframes subtitleFade {
|
| 129 |
+
from {
|
| 130 |
+
opacity: 0;
|
| 131 |
+
transform: translateY(30px);
|
| 132 |
+
}
|
| 133 |
+
to {
|
| 134 |
+
opacity: 1;
|
| 135 |
+
transform: translateY(0);
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* ๋ชจ๋ฐ์ผ ๋ฐ์ํ */
|
| 140 |
+
@media (max-width: 768px) {
|
| 141 |
+
.hero-title {
|
| 142 |
+
font-size: 2.5rem;
|
| 143 |
+
letter-spacing: 2px;
|
| 144 |
+
}
|
| 145 |
+
.hero-subtitle {
|
| 146 |
+
font-size: 1.1rem;
|
| 147 |
+
letter-spacing: 2px;
|
| 148 |
+
}
|
| 149 |
+
.hero-container {
|
| 150 |
+
padding: 5rem 1rem;
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 155 |
+
๐ฌ Chat Placeholder (๊ฐ์ด๋ ํ๋ฉด) - ๋คํฌ ๋ชจ๋
|
| 156 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 157 |
+
.chat-placeholder {
|
| 158 |
+
background: linear-gradient(135deg,
|
| 159 |
+
rgba(30,30,35,0.95) 0%,
|
| 160 |
+
rgba(25,25,30,0.95) 100%);
|
| 161 |
+
border-radius: 20px;
|
| 162 |
+
padding: 3rem;
|
| 163 |
+
text-align: left;
|
| 164 |
+
box-shadow:
|
| 165 |
+
0 15px 50px rgba(0,0,0,0.4),
|
| 166 |
+
0 0 0 1px rgba(102,126,234,0.2) inset;
|
| 167 |
+
animation: slideIn 0.7s ease-out;
|
| 168 |
+
backdrop-filter: blur(15px);
|
| 169 |
+
border: 1px solid rgba(102,126,234,0.2);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
@keyframes slideIn {
|
| 173 |
+
from {
|
| 174 |
+
opacity: 0;
|
| 175 |
+
transform: translateY(40px);
|
| 176 |
+
}
|
| 177 |
+
to {
|
| 178 |
+
opacity: 1;
|
| 179 |
+
transform: translateY(0);
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.placeholder-title {
|
| 184 |
+
font-size: 2rem;
|
| 185 |
+
font-weight: 900;
|
| 186 |
+
background: linear-gradient(135deg, #667eea, #f093fb);
|
| 187 |
+
-webkit-background-clip: text;
|
| 188 |
+
-webkit-text-fill-color: transparent;
|
| 189 |
+
background-clip: text;
|
| 190 |
+
margin-bottom: 2.5rem;
|
| 191 |
+
text-align: center;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.placeholder-steps {
|
| 195 |
+
margin: 2.5rem 0;
|
| 196 |
+
padding-left: 0;
|
| 197 |
+
line-height: 2.5;
|
| 198 |
+
color: rgba(255,255,255,0.9);
|
| 199 |
+
font-size: 1.1rem;
|
| 200 |
+
list-style: none;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.placeholder-steps li {
|
| 204 |
+
margin-bottom: 1.5rem;
|
| 205 |
+
padding-left: 3rem;
|
| 206 |
+
position: relative;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.placeholder-steps li::before {
|
| 210 |
+
content: 'โ';
|
| 211 |
+
position: absolute;
|
| 212 |
+
left: 0;
|
| 213 |
+
top: 0;
|
| 214 |
+
width: 35px;
|
| 215 |
+
height: 35px;
|
| 216 |
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
| 217 |
+
color: white;
|
| 218 |
+
border-radius: 50%;
|
| 219 |
+
display: flex;
|
| 220 |
+
align-items: center;
|
| 221 |
+
justify-content: center;
|
| 222 |
+
font-weight: bold;
|
| 223 |
+
font-size: 1.3rem;
|
| 224 |
+
box-shadow: 0 4px 15px rgba(102,126,234,0.4);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.placeholder-steps strong {
|
| 228 |
+
color: #7c92ff;
|
| 229 |
+
font-weight: 800;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.highlight-box {
|
| 233 |
+
background: linear-gradient(135deg,
|
| 234 |
+
rgba(255, 215, 0, 0.4),
|
| 235 |
+
rgba(255, 193, 7, 0.4));
|
| 236 |
+
padding: 4px 12px;
|
| 237 |
+
border-radius: 6px;
|
| 238 |
+
font-weight: 800;
|
| 239 |
+
color: #fff;
|
| 240 |
+
box-shadow: 0 2px 10px rgba(255, 215, 0, 0.3);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 244 |
+
๐ ๋ก๋ฉ ๋ฉ์์ง
|
| 245 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 246 |
+
.loading-message {
|
| 247 |
+
background: linear-gradient(135deg,
|
| 248 |
+
rgba(30,30,35,0.95) 0%,
|
| 249 |
+
rgba(25,25,30,0.95) 100%);
|
| 250 |
+
border-radius: 20px;
|
| 251 |
+
padding: 2rem;
|
| 252 |
+
text-align: center;
|
| 253 |
+
box-shadow:
|
| 254 |
+
0 10px 40px rgba(0,0,0,0.4),
|
| 255 |
+
0 0 0 1px rgba(102,126,234,0.2) inset;
|
| 256 |
+
animation: pulse 2s ease-in-out infinite;
|
| 257 |
+
backdrop-filter: blur(15px);
|
| 258 |
+
border: 1px solid rgba(102,126,234,0.2);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
@keyframes pulse {
|
| 262 |
+
0%, 100% {
|
| 263 |
+
opacity: 1;
|
| 264 |
+
transform: scale(1);
|
| 265 |
+
}
|
| 266 |
+
50% {
|
| 267 |
+
opacity: 0.8;
|
| 268 |
+
transform: scale(1.02);
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.loading-message-text {
|
| 273 |
+
font-size: 1.3rem;
|
| 274 |
+
font-weight: 600;
|
| 275 |
+
background: linear-gradient(135deg, #667eea, #f093fb);
|
| 276 |
+
-webkit-background-clip: text;
|
| 277 |
+
-webkit-text-fill-color: transparent;
|
| 278 |
+
background-clip: text;
|
| 279 |
+
margin-bottom: 1rem;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.loading-dots {
|
| 283 |
+
display: inline-block;
|
| 284 |
+
animation: dots 1.5s steps(4, end) infinite;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
@keyframes dots {
|
| 288 |
+
0%, 20% { content: '.'; }
|
| 289 |
+
40% { content: '..'; }
|
| 290 |
+
60%, 100% { content: '...'; }
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 294 |
+
๐ ์ถ์ฒ ๋ฒํผ ์คํ์ผ
|
| 295 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 296 |
+
.stButton button {
|
| 297 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 298 |
+
border-radius: 10px;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.stButton button:hover {
|
| 302 |
+
transform: translateY(-3px);
|
| 303 |
+
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.stButton button:active {
|
| 307 |
+
transform: translateY(-1px);
|
| 308 |
+
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 312 |
+
๐จ ์ฌ์ด๋๋ฐ ์คํ์ผ
|
| 313 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 314 |
+
[data-testid="stSidebar"] {
|
| 315 |
+
background: linear-gradient(180deg,
|
| 316 |
+
#667eea 0%,
|
| 317 |
+
#764ba2 100%);
|
| 318 |
+
box-shadow: 4px 0 30px rgba(0,0,0,0.2);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
[data-testid="stSidebar"] h1 {
|
| 322 |
+
color: white !important;
|
| 323 |
+
text-shadow: 2px 2px 15px rgba(0,0,0,0.4);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
[data-testid="stSidebar"] .stButton button {
|
| 327 |
+
background: rgba(255,255,255,0.2);
|
| 328 |
+
color: white;
|
| 329 |
+
border: 2px solid rgba(255,255,255,0.3);
|
| 330 |
+
font-weight: 700;
|
| 331 |
+
backdrop-filter: blur(10px);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
[data-testid="stSidebar"] .stButton button:hover {
|
| 335 |
+
background: rgba(255,255,255,0.3);
|
| 336 |
+
border-color: rgba(255,255,255,0.5);
|
| 337 |
+
transform: translateY(-2px);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/* ํ์ผ ์
๋ก๋ ์คํ์ผ */
|
| 341 |
+
[data-testid="stSidebar"] [data-testid="stFileUploader"] {
|
| 342 |
+
background: rgba(255,255,255,0.15);
|
| 343 |
+
border-radius: 15px;
|
| 344 |
+
padding: 1.5rem;
|
| 345 |
+
border: 3px dashed rgba(255,255,255,0.4);
|
| 346 |
+
transition: all 0.3s ease;
|
| 347 |
+
backdrop-filter: blur(10px);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
[data-testid="stSidebar"] [data-testid="stFileUploader"]:hover {
|
| 351 |
+
background: rgba(255,255,255,0.25);
|
| 352 |
+
border-color: rgba(255,255,255,0.6);
|
| 353 |
+
transform: scale(1.02);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
[data-testid="stSidebar"] [data-testid="stFileUploader"] label {
|
| 357 |
+
color: white !important;
|
| 358 |
+
font-weight: 600;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 362 |
+
๐ PDF ํ์ด๋ผ์ดํธ (๋งค์ฐ ์งํ ๊ณจ๋)
|
| 363 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 364 |
+
.highlight-text {
|
| 365 |
+
background: linear-gradient(135deg,
|
| 366 |
+
rgba(255, 193, 7, 0.95),
|
| 367 |
+
rgba(255, 152, 0, 0.95)) !important;
|
| 368 |
+
padding: 4px 8px;
|
| 369 |
+
border-radius: 5px;
|
| 370 |
+
font-weight: 800;
|
| 371 |
+
color: #1a1a1a !important;
|
| 372 |
+
box-shadow:
|
| 373 |
+
0 0 15px rgba(255, 193, 7, 0.7),
|
| 374 |
+
0 3px 8px rgba(0,0,0,0.3);
|
| 375 |
+
animation: highlightPulse 2.5s ease-in-out infinite;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
@keyframes highlightPulse {
|
| 379 |
+
0%, 100% {
|
| 380 |
+
box-shadow:
|
| 381 |
+
0 0 15px rgba(255, 193, 7, 0.7),
|
| 382 |
+
0 3px 8px rgba(0,0,0,0.3);
|
| 383 |
+
}
|
| 384 |
+
50% {
|
| 385 |
+
box-shadow:
|
| 386 |
+
0 0 25px rgba(255, 193, 7, 0.9),
|
| 387 |
+
0 5px 15px rgba(0,0,0,0.4);
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 392 |
+
๐ฌ ์ฑํ
๋ฉ์์ง ์คํ์ผ - ๋คํฌ ๋ชจ๋
|
| 393 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 394 |
+
[data-testid="stChatMessage"] {
|
| 395 |
+
border-radius: 15px;
|
| 396 |
+
padding: 1.5rem;
|
| 397 |
+
margin-bottom: 1.2rem;
|
| 398 |
+
animation: messageSlide 0.4s ease-out;
|
| 399 |
+
background: rgba(30,30,35,0.6) !important;
|
| 400 |
+
border: 1px solid rgba(102,126,234,0.15);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
@keyframes messageSlide {
|
| 404 |
+
from {
|
| 405 |
+
opacity: 0;
|
| 406 |
+
transform: translateX(-30px);
|
| 407 |
+
}
|
| 408 |
+
to {
|
| 409 |
+
opacity: 1;
|
| 410 |
+
transform: translateX(0);
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* ์ฌ์ฉ์ ๋ฉ์์ง */
|
| 415 |
+
[data-testid="stChatMessage"][data-testid*="user"] {
|
| 416 |
+
background: rgba(102,126,234,0.15) !important;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
/* AI ๋ฉ์์ง */
|
| 420 |
+
[data-testid="stChatMessage"][data-testid*="assistant"] {
|
| 421 |
+
background: rgba(30,30,35,0.8) !important;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 425 |
+
๐ ์คํฌ๋กค๋ฐ ์คํ์ผ
|
| 426 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 427 |
+
::-webkit-scrollbar {
|
| 428 |
+
width: 12px;
|
| 429 |
+
height: 12px;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
::-webkit-scrollbar-track {
|
| 433 |
+
background: rgba(0,0,0,0.2);
|
| 434 |
+
border-radius: 10px;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
::-webkit-scrollbar-thumb {
|
| 438 |
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
| 439 |
+
border-radius: 10px;
|
| 440 |
+
border: 2px solid rgba(0,0,0,0.2);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
::-webkit-scrollbar-thumb:hover {
|
| 444 |
+
background: linear-gradient(135deg, #764ba2, #f093fb);
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 448 |
+
๐ฏ ๋ก๋ฉ ์คํผ๋ ์ปค์คํ
|
| 449 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 450 |
+
.stSpinner > div {
|
| 451 |
+
border-top-color: #667eea !important;
|
| 452 |
+
border-right-color: #764ba2 !important;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 456 |
+
โจ ๋ฉ์ธ ์ปจํ
์ธ ์์ญ - ๋คํฌ ๋ฐฐ๊ฒฝ
|
| 457 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 458 |
+
.main .block-container {
|
| 459 |
+
padding-top: 1rem !important;
|
| 460 |
+
padding-bottom: 1rem !important;
|
| 461 |
+
background: transparent;
|
| 462 |
+
max-width: 100% !important;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 466 |
+
๐ฏ Streamlit ๊ธฐ๋ณธ ์ปจํ
์ด๋ ํ
๋๋ฆฌ ์ ๊ฑฐ
|
| 467 |
+
โโโโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 468 |
+
[data-testid="stVerticalBlock"],
|
| 469 |
+
[data-testid="stHorizontalBlock"],
|
| 470 |
+
[data-testid="column"] {
|
| 471 |
+
border: none !important;
|
| 472 |
+
box-shadow: none !important;
|
| 473 |
+
background: transparent !important;
|
| 474 |
+
padding: 0 !important;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
/* PDF ๋ทฐ์ด - ๊น๋ํ ๋์์ธ */
|
| 478 |
+
[data-testid="stVerticalBlock"] iframe {
|
| 479 |
+
border-radius: 12px !important;
|
| 480 |
+
box-shadow: 0 8px 30px rgba(0,0,0,0.3) !important;
|
| 481 |
+
border: 1px solid rgba(102,126,234,0.2) !important;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
/* ์ฑํ
์์ญ - ํ
๋๋ฆฌ ์์ด ๊น๋ํ๊ฒ */
|
| 485 |
+
[data-testid="column"]:last-child {
|
| 486 |
+
background: transparent !important;
|
| 487 |
+
border: none !important;
|
| 488 |
+
padding: 0.5rem !important;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 492 |
+
๐ ํ์ผ ์
๋ก๋ ๋ฒํผ ํ
์คํธ ๋ณ๊ฒฝ
|
| 493 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 494 |
+
[data-testid="stFileUploader"] button[kind="secondary"] {
|
| 495 |
+
font-size: 0 !important;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
[data-testid="stFileUploader"] button[kind="secondary"]::after {
|
| 499 |
+
content: "ํ์ผ ์ฐพ๊ธฐ";
|
| 500 |
+
font-size: 1rem;
|
| 501 |
+
font-weight: 600;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
</style>
|
| 506 |
+
|
| 507 |
+
<!-- ์ ๋๋ฉ์ด์
๋ฐฐ๊ฒฝ -->
|
| 508 |
+
<div class="animated-background"></div>
|
| 509 |
+
"""
|
utils/__init__.py
ADDED
|
File without changes
|
utils/export_utils.py
ADDED
|
File without changes
|
utils/helpers.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/helpers.py
|
| 2 |
+
"""์ ํธ๋ฆฌํฐ ํจ์๋ค"""
|
| 3 |
+
import os
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def ensure_dir(directory: str):
|
| 8 |
+
"""
|
| 9 |
+
๋๋ ํ ๋ฆฌ๊ฐ ์์ผ๋ฉด ์์ฑ
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
directory: ๋๋ ํ ๋ฆฌ ๊ฒฝ๋ก
|
| 13 |
+
"""
|
| 14 |
+
if not os.path.exists(directory):
|
| 15 |
+
os.makedirs(directory)
|
| 16 |
+
print(f"๐ ๋๋ ํ ๋ฆฌ ์์ฑ: {directory}")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def format_file_size(size_bytes: int) -> str:
|
| 20 |
+
"""
|
| 21 |
+
ํ์ผ ํฌ๊ธฐ๋ฅผ ์ฝ๊ธฐ ์ฌ์ด ํ์์ผ๋ก ๋ณํ
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
size_bytes: ๋ฐ์ดํธ ๋จ์ ํฌ๊ธฐ
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
str: ํฌ๋งท๋ ํฌ๊ธฐ (์: "1.5 MB")
|
| 28 |
+
"""
|
| 29 |
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
| 30 |
+
if size_bytes < 1024.0:
|
| 31 |
+
return f"{size_bytes:.1f} {unit}"
|
| 32 |
+
size_bytes /= 1024.0
|
| 33 |
+
return f"{size_bytes:.1f} TB"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str:
|
| 37 |
+
"""
|
| 38 |
+
ํ
์คํธ๋ฅผ ์ง์ ๋ ๊ธธ์ด๋ก ์๋ฅด๊ธฐ
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
text: ์๋ณธ ํ
์คํธ
|
| 42 |
+
max_length: ์ต๋ ๊ธธ์ด
|
| 43 |
+
suffix: ์๋ฆฐ ๊ฒฝ์ฐ ์ถ๊ฐํ ์ ๋ฏธ์ฌ
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
str: ์๋ฆฐ ํ
์คํธ
|
| 47 |
+
"""
|
| 48 |
+
if len(text) <= max_length:
|
| 49 |
+
return text
|
| 50 |
+
return text[:max_length - len(suffix)] + suffix
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def safe_get(dictionary: dict, key: str, default: Optional[any] = None):
|
| 54 |
+
"""
|
| 55 |
+
์์ ํ๊ฒ ๋์
๋๋ฆฌ ๊ฐ ๊ฐ์ ธ์ค๊ธฐ
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
dictionary: ๋์
๋๋ฆฌ
|
| 59 |
+
key: ํค
|
| 60 |
+
default: ๊ธฐ๋ณธ๊ฐ
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
๊ฐ ๋๋ ๊ธฐ๋ณธ๊ฐ
|
| 64 |
+
"""
|
| 65 |
+
try:
|
| 66 |
+
return dictionary.get(key, default)
|
| 67 |
+
except:
|
| 68 |
+
return default
|
| 69 |
+
|
utils/logger.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/logger.py
|
| 2 |
+
"""๋ก๊น
์ ํธ๋ฆฌํฐ"""
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def setup_logger(name: str = "TEAM_EA", level: int = logging.INFO) -> logging.Logger:
|
| 8 |
+
"""
|
| 9 |
+
๋ก๊ฑฐ ์ค์
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
name: ๋ก๊ฑฐ ์ด๋ฆ
|
| 13 |
+
level: ๋ก๊น
๋ ๋ฒจ
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
logging.Logger: ์ค์ ๋ ๋ก๊ฑฐ
|
| 17 |
+
"""
|
| 18 |
+
logger = logging.getLogger(name)
|
| 19 |
+
logger.setLevel(level)
|
| 20 |
+
|
| 21 |
+
# ํธ๋ค๋ฌ๊ฐ ์ด๋ฏธ ์์ผ๋ฉด ์ถ๊ฐํ์ง ์์
|
| 22 |
+
if not logger.handlers:
|
| 23 |
+
# ์ฝ์ ํธ๋ค๋ฌ
|
| 24 |
+
console_handler = logging.StreamHandler()
|
| 25 |
+
console_handler.setLevel(level)
|
| 26 |
+
|
| 27 |
+
# ํฌ๋งทํฐ
|
| 28 |
+
formatter = logging.Formatter(
|
| 29 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 30 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 31 |
+
)
|
| 32 |
+
console_handler.setFormatter(formatter)
|
| 33 |
+
|
| 34 |
+
logger.addHandler(console_handler)
|
| 35 |
+
|
| 36 |
+
return logger
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ์ ์ญ ๋ก๊ฑฐ
|
| 40 |
+
logger = setup_logger()
|
| 41 |
+
|
utils/pdf_utils.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/pdf_utils.py
|
| 2 |
+
"""PDF ํ
์คํธ ์ขํ ์ถ์ถ ์ ํธ"""
|
| 3 |
+
import fitz # PyMuPDF
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def get_text_coordinates(pdf_path: str, page_num: int, search_text: str) -> List[Dict]:
|
| 8 |
+
"""
|
| 9 |
+
PDF์์ ํน์ ํ
์คํธ์ ์ขํ๋ฅผ ์ฐพ์ ํ์ด๋ผ์ดํธ ์ ๋ณด ๋ฐํ
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
pdf_path: PDF ํ์ผ ๊ฒฝ๋ก
|
| 13 |
+
page_num: ํ์ด์ง ๋ฒํธ (1-based)
|
| 14 |
+
search_text: ๊ฒ์ํ ํ
์คํธ
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
ํ์ด๋ผ์ดํธ ์ ๋ณด ๋ฆฌ์คํธ
|
| 18 |
+
"""
|
| 19 |
+
try:
|
| 20 |
+
doc = fitz.open(pdf_path)
|
| 21 |
+
page = doc[page_num - 1]
|
| 22 |
+
|
| 23 |
+
# ๊ฒ์ ํ
์คํธ ์ ๋ฆฌ (๋๋ฌด ๊ธธ๋ฉด ์๋ถ๋ถ๋ง)
|
| 24 |
+
search_query = search_text[:100].strip()
|
| 25 |
+
text_instances = page.search_for(search_query)
|
| 26 |
+
|
| 27 |
+
annotations = []
|
| 28 |
+
|
| 29 |
+
for rect in text_instances:
|
| 30 |
+
# streamlit-pdf-viewer ํ์ - ํ๊ดํ ์คํ์ผ
|
| 31 |
+
annotations.append({
|
| 32 |
+
"page": page_num, # 1-based index
|
| 33 |
+
"x": rect.x0,
|
| 34 |
+
"y": rect.y0,
|
| 35 |
+
"width": rect.x1 - rect.x0,
|
| 36 |
+
"height": rect.y1 - rect.y0,
|
| 37 |
+
"color": "#FFFF00", # ๋ฐ์ ๋
ธ๋์
|
| 38 |
+
"opacity": 0.4 # 40% ํฌ๋ช
๋ (ํ๊ดํ ํจ๊ณผ)
|
| 39 |
+
})
|
| 40 |
+
|
| 41 |
+
doc.close()
|
| 42 |
+
|
| 43 |
+
if annotations:
|
| 44 |
+
print(f" โ
{len(annotations)}๊ฐ ํ์ด๋ผ์ดํธ ์์ฑ (ํ์ด์ง {page_num})")
|
| 45 |
+
else:
|
| 46 |
+
print(f" โ ๏ธ ํ
์คํธ '{search_query[:30]}...' ์ฐพ์ง ๋ชปํจ")
|
| 47 |
+
|
| 48 |
+
return annotations
|
| 49 |
+
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"โ ํ์ด๋ผ์ดํธ ์์ฑ ์ค๋ฅ: {e}")
|
| 52 |
+
return []
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
result = get_text_coordinates("test.pdf", 1, "sample text")
|
| 57 |
+
print(f"ํ์ด๋ผ์ดํธ ๊ฐ์: {len(result)}")
|
utils/session_manager.py
ADDED
|
File without changes
|
utils/sidebar_components.py
ADDED
|
File without changes
|