Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Update app.py
Browse files
app.py
CHANGED
@@ -7,6 +7,8 @@ import asyncio
|
|
7 |
import logging
|
8 |
import threading
|
9 |
import concurrent.futures
|
|
|
|
|
10 |
|
11 |
# λ‘κΉ
μ€μ
|
12 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
@@ -37,15 +39,26 @@ if not METADATA_DIR.exists():
|
|
37 |
METADATA_DIR.mkdir(parents=True)
|
38 |
PDF_METADATA_FILE = METADATA_DIR / "pdf_metadata.json"
|
39 |
|
|
|
|
|
|
|
|
|
|
|
40 |
# κ΄λ¦¬μ λΉλ°λ²νΈ
|
41 |
ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # νκ²½ λ³μμμ κ°μ Έμ€κΈ°, κΈ°λ³Έκ°μ ν
μ€νΈμ©
|
42 |
|
|
|
|
|
|
|
|
|
43 |
# μ μ μΊμ κ°μ²΄
|
44 |
pdf_cache: Dict[str, Dict[str, Any]] = {}
|
45 |
# μΊμ± λ½
|
46 |
cache_locks = {}
|
47 |
# PDF λ©νλ°μ΄ν° (ID to κ²½λ‘ λ§€ν)
|
48 |
pdf_metadata: Dict[str, str] = {}
|
|
|
|
|
49 |
|
50 |
# PDF λ©νλ°μ΄ν° λ‘λ
|
51 |
def load_pdf_metadata():
|
@@ -69,7 +82,6 @@ def save_pdf_metadata():
|
|
69 |
except Exception as e:
|
70 |
logger.error(f"λ©νλ°μ΄ν° μ μ₯ μ€λ₯: {e}")
|
71 |
|
72 |
-
# PDF ID μμ± (νμΌλͺ
+ νμμ€ν¬ν κΈ°λ°)
|
73 |
# PDF ID μμ± (νμΌλͺ
+ νμμ€ν¬ν κΈ°λ°) - λ λ¨μνκ³ μμ ν λ°©μμΌλ‘ λ³κ²½
|
74 |
def generate_pdf_id(filename: str) -> str:
|
75 |
# νμΌλͺ
μμ νμ₯μ μ κ±°
|
@@ -83,8 +95,6 @@ def generate_pdf_id(filename: str) -> str:
|
|
83 |
random_suffix = uuid.uuid4().hex[:6]
|
84 |
return f"{safe_name}_{timestamp}_{random_suffix}"
|
85 |
|
86 |
-
|
87 |
-
|
88 |
# PDF νμΌ λͺ©λ‘ κ°μ Έμ€κΈ° (λ©μΈ λλ ν 리μ©)
|
89 |
def get_pdf_files():
|
90 |
pdf_files = []
|
@@ -146,6 +156,164 @@ def generate_pdf_projects():
|
|
146 |
def get_cache_path(pdf_name: str):
|
147 |
return CACHE_DIR / f"{pdf_name}_cache.json"
|
148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
149 |
# μ΅μ νλ PDF νμ΄μ§ μΊμ± ν¨μ
|
150 |
async def cache_pdf(pdf_path: str):
|
151 |
try:
|
@@ -300,7 +468,6 @@ async def cache_pdf(pdf_path: str):
|
|
300 |
pdf_cache[pdf_name]["status"] = "error"
|
301 |
pdf_cache[pdf_name]["error"] = str(e)
|
302 |
|
303 |
-
# PDF IDλ‘ PDF κ²½λ‘ μ°ΎκΈ°
|
304 |
# PDF IDλ‘ PDF κ²½λ‘ μ°ΎκΈ° (κ°μ λ κ²μ λ‘μ§)
|
305 |
def get_pdf_path_by_id(pdf_id: str) -> str:
|
306 |
logger.info(f"PDF IDλ‘ νμΌ μ‘°ν: {pdf_id}")
|
@@ -534,6 +701,50 @@ async def get_cache_status(path: str = None):
|
|
534 |
return {name: {"status": info["status"], "progress": info.get("progress", 0)}
|
535 |
for name, info in pdf_cache.items()}
|
536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
537 |
# API μλν¬μΈνΈ: μΊμλ PDF μ½ν
μΈ μ 곡 (μ μ§μ λ‘λ© μ§μ)
|
538 |
@app.get("/api/cached-pdf")
|
539 |
async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
|
@@ -818,7 +1029,7 @@ async def root(request: Request, pdf_id: Optional[str] = Query(None)):
|
|
818 |
return RedirectResponse(url=f"/view/{pdf_id}")
|
819 |
return get_html_content()
|
820 |
|
821 |
-
# HTML λ¬Έμμ΄ (UI
|
822 |
HTML = """
|
823 |
<!doctype html>
|
824 |
<html lang="ko">
|
@@ -844,6 +1055,8 @@ HTML = """
|
|
844 |
--secondary-color: #ffd6e0; /* νμ€ν
νν¬ */
|
845 |
--tertiary-color: #c3fae8; /* νμ€ν
λ―ΌνΈ */
|
846 |
--accent-color: #d0bfff; /* νμ€ν
νΌν */
|
|
|
|
|
847 |
--bg-color: #f8f9fa; /* λ°μ λ°°κ²½ */
|
848 |
--text-color: #495057; /* λΆλλ¬μ΄ μ΄λμ΄ μ */
|
849 |
--card-bg: #ffffff; /* μΉ΄λ λ°°κ²½μ */
|
@@ -873,7 +1086,7 @@ HTML = """
|
|
873 |
}
|
874 |
|
875 |
/* ν€λ μ λͺ© μ κ±° λ° Home λ²νΌ λ μ΄μ΄ μ²λ¦¬ */
|
876 |
-
.floating-home {
|
877 |
position: fixed;
|
878 |
top: 20px;
|
879 |
left: 20px;
|
@@ -892,12 +1105,17 @@ HTML = """
|
|
892 |
overflow: hidden;
|
893 |
}
|
894 |
|
895 |
-
.floating-
|
|
|
|
|
|
|
|
|
|
|
896 |
transform: scale(1.05);
|
897 |
box-shadow: var(--shadow-lg);
|
898 |
}
|
899 |
|
900 |
-
.floating-home .icon {
|
901 |
display: flex;
|
902 |
justify-content: center;
|
903 |
align-items: center;
|
@@ -908,11 +1126,19 @@ HTML = """
|
|
908 |
transition: var(--transition);
|
909 |
}
|
910 |
|
|
|
|
|
|
|
|
|
911 |
.floating-home:hover .icon {
|
912 |
color: #8bc5f8;
|
913 |
}
|
914 |
|
915 |
-
.floating-
|
|
|
|
|
|
|
|
|
916 |
position: absolute;
|
917 |
left: 70px;
|
918 |
background: rgba(255, 255, 255, 0.95);
|
@@ -928,7 +1154,7 @@ HTML = """
|
|
928 |
transition: all 0.3s ease;
|
929 |
}
|
930 |
|
931 |
-
.floating-home:hover .title {
|
932 |
opacity: 1;
|
933 |
transform: translateX(0);
|
934 |
}
|
@@ -1472,6 +1698,257 @@ HTML = """
|
|
1472 |
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
1473 |
}
|
1474 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1475 |
/* λ°μν λμμΈ */
|
1476 |
@media (max-width: 768px) {
|
1477 |
.grid {
|
@@ -1488,12 +1965,12 @@ HTML = """
|
|
1488 |
padding: 10px 20px;
|
1489 |
}
|
1490 |
|
1491 |
-
.floating-home {
|
1492 |
width: 50px;
|
1493 |
height: 50px;
|
1494 |
}
|
1495 |
|
1496 |
-
.floating-home .icon {
|
1497 |
font-size: 18px;
|
1498 |
}
|
1499 |
|
@@ -1501,6 +1978,10 @@ HTML = """
|
|
1501 |
padding: 6px 15px;
|
1502 |
font-size: 12px;
|
1503 |
}
|
|
|
|
|
|
|
|
|
1504 |
}
|
1505 |
</style>
|
1506 |
</head>
|
@@ -1511,6 +1992,25 @@ HTML = """
|
|
1511 |
<div class="title">νμΌλ‘ λμκ°κΈ°</div>
|
1512 |
</div>
|
1513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1514 |
<!-- κ΄λ¦¬μ λ²νΌ -->
|
1515 |
<div id="adminButton">
|
1516 |
<i class="fas fa-cog"></i> Admin
|
@@ -1588,6 +2088,11 @@ HTML = """
|
|
1588 |
let audioInitialized = false;
|
1589 |
let audioContext = null;
|
1590 |
|
|
|
|
|
|
|
|
|
|
|
1591 |
// μ€λμ€ μ΄κΈ°ν ν¨μ
|
1592 |
function initializeAudio() {
|
1593 |
if (audioInitialized) return Promise.resolve();
|
@@ -1685,6 +2190,158 @@ HTML = """
|
|
1685 |
/* ββ μ νΈ ββ */
|
1686 |
function $id(id){return document.getElementById(id)}
|
1687 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1688 |
// DOMμ΄ λ‘λλλ©΄ μ€ν
|
1689 |
document.addEventListener('DOMContentLoaded', function() {
|
1690 |
console.log("DOM λ‘λ μλ£, μ΄λ²€νΈ μ€μ μμ");
|
@@ -1746,6 +2403,40 @@ HTML = """
|
|
1746 |
$id('loadingPages').style.display = 'none';
|
1747 |
currentLoadingPdfPath = null;
|
1748 |
currentPdfId = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1749 |
});
|
1750 |
}
|
1751 |
});
|
@@ -2015,6 +2706,8 @@ HTML = """
|
|
2015 |
createFlipBook(cachedData.pages);
|
2016 |
// νμ¬ μ΄λ¦° PDFμ ID μ μ₯
|
2017 |
currentPdfId = pdfId;
|
|
|
|
|
2018 |
return;
|
2019 |
}
|
2020 |
} catch (error) {
|
@@ -2054,6 +2747,8 @@ HTML = """
|
|
2054 |
|
2055 |
// νμ¬ μ΄λ¦° PDFμ ID μ μ₯
|
2056 |
currentPdfId = pdfId;
|
|
|
|
|
2057 |
}
|
2058 |
} catch (error) {
|
2059 |
console.error("PDF IDλ‘ μ΄κΈ° μ€ν¨:", error);
|
@@ -2092,10 +2787,19 @@ HTML = """
|
|
2092 |
const card = document.querySelectorAll('.card')[i];
|
2093 |
if (card && card.dataset.pdfId) {
|
2094 |
currentPdfId = card.dataset.pdfId;
|
|
|
|
|
2095 |
} else {
|
2096 |
currentPdfId = null;
|
|
|
|
|
2097 |
}
|
2098 |
|
|
|
|
|
|
|
|
|
|
|
2099 |
// κΈ°μ‘΄ FlipBook μ 리
|
2100 |
if(fb) {
|
2101 |
fb.destroy();
|
@@ -2430,6 +3134,14 @@ HTML = """
|
|
2430 |
$id('homeButton').style.display=showHome?'none':'block';
|
2431 |
$id('adminPage').style.display='none';
|
2432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2433 |
// λ·°μ΄ λͺ¨λμΌ λ μ€νμΌ λ³κ²½
|
2434 |
if(!showHome) {
|
2435 |
document.body.classList.add('viewer-mode');
|
|
|
7 |
import logging
|
8 |
import threading
|
9 |
import concurrent.futures
|
10 |
+
from openai import OpenAI
|
11 |
+
import fitz # PyMuPDF
|
12 |
|
13 |
# λ‘κΉ
μ€μ
|
14 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
39 |
METADATA_DIR.mkdir(parents=True)
|
40 |
PDF_METADATA_FILE = METADATA_DIR / "pdf_metadata.json"
|
41 |
|
42 |
+
# μλ² λ© μΊμ λλ ν 리 μ€μ
|
43 |
+
EMBEDDING_DIR = pathlib.Path("/data/embeddings") if os.path.exists("/data") else BASE / "embeddings"
|
44 |
+
if not EMBEDDING_DIR.exists():
|
45 |
+
EMBEDDING_DIR.mkdir(parents=True)
|
46 |
+
|
47 |
# κ΄λ¦¬μ λΉλ°λ²νΈ
|
48 |
ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # νκ²½ λ³μμμ κ°μ Έμ€κΈ°, κΈ°λ³Έκ°μ ν
μ€νΈμ©
|
49 |
|
50 |
+
# OpenAI API ν€ μ€μ
|
51 |
+
OPENAI_API_KEY = os.getenv("LLM_API", "")
|
52 |
+
openai_client = OpenAI(api_key=OPENAI_API_KEY)
|
53 |
+
|
54 |
# μ μ μΊμ κ°μ²΄
|
55 |
pdf_cache: Dict[str, Dict[str, Any]] = {}
|
56 |
# μΊμ± λ½
|
57 |
cache_locks = {}
|
58 |
# PDF λ©νλ°μ΄ν° (ID to κ²½λ‘ λ§€ν)
|
59 |
pdf_metadata: Dict[str, str] = {}
|
60 |
+
# PDF μλ² λ© μΊμ
|
61 |
+
pdf_embeddings: Dict[str, Dict[str, Any]] = {}
|
62 |
|
63 |
# PDF λ©νλ°μ΄ν° λ‘λ
|
64 |
def load_pdf_metadata():
|
|
|
82 |
except Exception as e:
|
83 |
logger.error(f"λ©νλ°μ΄ν° μ μ₯ μ€λ₯: {e}")
|
84 |
|
|
|
85 |
# PDF ID μμ± (νμΌλͺ
+ νμμ€ν¬ν κΈ°λ°) - λ λ¨μνκ³ μμ ν λ°©μμΌλ‘ λ³κ²½
|
86 |
def generate_pdf_id(filename: str) -> str:
|
87 |
# νμΌλͺ
μμ νμ₯μ μ κ±°
|
|
|
95 |
random_suffix = uuid.uuid4().hex[:6]
|
96 |
return f"{safe_name}_{timestamp}_{random_suffix}"
|
97 |
|
|
|
|
|
98 |
# PDF νμΌ λͺ©λ‘ κ°μ Έμ€κΈ° (λ©μΈ λλ ν 리μ©)
|
99 |
def get_pdf_files():
|
100 |
pdf_files = []
|
|
|
156 |
def get_cache_path(pdf_name: str):
|
157 |
return CACHE_DIR / f"{pdf_name}_cache.json"
|
158 |
|
159 |
+
# μλ² λ© μΊμ νμΌ κ²½λ‘ μμ±
|
160 |
+
def get_embedding_path(pdf_id: str):
|
161 |
+
return EMBEDDING_DIR / f"{pdf_id}_embedding.json"
|
162 |
+
|
163 |
+
# PDF ν
μ€νΈ μΆμΆ ν¨μ
|
164 |
+
def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
|
165 |
+
try:
|
166 |
+
doc = fitz.open(pdf_path)
|
167 |
+
chunks = []
|
168 |
+
|
169 |
+
for page_num in range(len(doc)):
|
170 |
+
page = doc[page_num]
|
171 |
+
text = page.get_text()
|
172 |
+
|
173 |
+
# νμ΄μ§ ν
μ€νΈκ° μλ κ²½μ°λ§ μΆκ°
|
174 |
+
if text.strip():
|
175 |
+
chunks.append({
|
176 |
+
"page": page_num + 1,
|
177 |
+
"text": text,
|
178 |
+
"chunk_id": f"page_{page_num + 1}"
|
179 |
+
})
|
180 |
+
|
181 |
+
return chunks
|
182 |
+
except Exception as e:
|
183 |
+
logger.error(f"PDF ν
μ€νΈ μΆμΆ μ€λ₯: {e}")
|
184 |
+
return []
|
185 |
+
|
186 |
+
# PDF IDλ‘ μλ² λ© μμ± λλ κ°μ Έμ€κΈ°
|
187 |
+
async def get_pdf_embedding(pdf_id: str) -> Dict[str, Any]:
|
188 |
+
try:
|
189 |
+
# μλ² λ© μΊμ νμΈ
|
190 |
+
embedding_path = get_embedding_path(pdf_id)
|
191 |
+
if embedding_path.exists():
|
192 |
+
try:
|
193 |
+
with open(embedding_path, "r", encoding="utf-8") as f:
|
194 |
+
return json.load(f)
|
195 |
+
except Exception as e:
|
196 |
+
logger.error(f"μλ² λ© μΊμ λ‘λ μ€λ₯: {e}")
|
197 |
+
|
198 |
+
# PDF κ²½λ‘ μ°ΎκΈ°
|
199 |
+
pdf_path = get_pdf_path_by_id(pdf_id)
|
200 |
+
if not pdf_path:
|
201 |
+
raise ValueError(f"PDF ID {pdf_id}μ ν΄λΉνλ νμΌμ μ°Ύμ μ μμ΅λλ€")
|
202 |
+
|
203 |
+
# ν
μ€νΈ μΆμΆ
|
204 |
+
chunks = extract_pdf_text(pdf_path)
|
205 |
+
if not chunks:
|
206 |
+
raise ValueError(f"PDFμμ ν
μ€νΈλ₯Ό μΆμΆν μ μμ΅λλ€: {pdf_path}")
|
207 |
+
|
208 |
+
# μλ² λ© μ μ₯ λ° λ°ν
|
209 |
+
embedding_data = {
|
210 |
+
"pdf_id": pdf_id,
|
211 |
+
"pdf_path": pdf_path,
|
212 |
+
"chunks": chunks,
|
213 |
+
"created_at": time.time()
|
214 |
+
}
|
215 |
+
|
216 |
+
# μλ² λ© μΊμ μ μ₯
|
217 |
+
with open(embedding_path, "w", encoding="utf-8") as f:
|
218 |
+
json.dump(embedding_data, f, ensure_ascii=False)
|
219 |
+
|
220 |
+
return embedding_data
|
221 |
+
|
222 |
+
except Exception as e:
|
223 |
+
logger.error(f"PDF μλ² λ© μμ± μ€λ₯: {e}")
|
224 |
+
return {"error": str(e), "pdf_id": pdf_id}
|
225 |
+
|
226 |
+
# PDF λ΄μ© κΈ°λ° μ§μμλ΅
|
227 |
+
async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
228 |
+
try:
|
229 |
+
# μλ² λ© λ°μ΄ν° κ°μ Έμ€κΈ°
|
230 |
+
embedding_data = await get_pdf_embedding(pdf_id)
|
231 |
+
if "error" in embedding_data:
|
232 |
+
return {"error": embedding_data["error"]}
|
233 |
+
|
234 |
+
# μ²ν¬ ν
μ€νΈ λͺ¨μΌκΈ° (μμλ‘ κ°λ¨νκ² μ 체 ν
μ€νΈ μ¬μ©)
|
235 |
+
all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
|
236 |
+
|
237 |
+
# OpenAI API νΈμΆ
|
238 |
+
# 컨ν
μ€νΈ ν¬κΈ°λ₯Ό κ³ λ €νμ¬ ν
μ€νΈκ° λ무 κΈΈλ©΄ μλΆλΆλ§ μ¬μ©
|
239 |
+
max_context_length = 60000 # ν ν° μκ° μλ λ¬Έμ μ κΈ°μ€ (λλ΅μ μΈ μ ν)
|
240 |
+
if len(all_text) > max_context_length:
|
241 |
+
all_text = all_text[:max_context_length] + "...(μ΄ν μλ΅)"
|
242 |
+
|
243 |
+
# μμ€ν
ν둬ννΈ μ€λΉ
|
244 |
+
system_prompt = """
|
245 |
+
λΉμ μ PDF λ΄μ©μ κΈ°λ°μΌλ‘ μ§λ¬Έμ λ΅λ³νλ λμ°λ―Έμ
λλ€. μ 곡λ PDF 컨ν
μ€νΈ μ 보λ§μ μ¬μ©νμ¬ λ΅λ³νμΈμ.
|
246 |
+
컨ν
μ€νΈμ κ΄λ ¨ μ λ³΄κ° μλ κ²½μ°, 'μ 곡λ PDFμμ ν΄λΉ μ 보λ₯Ό μ°Ύμ μ μμ΅λλ€'λΌκ³ μμ§ν λ΅νμΈμ.
|
247 |
+
λ΅λ³μ λͺ
ννκ³ κ°κ²°νκ² μμ±νκ³ , κ΄λ ¨ νμ΄μ§ λ²νΈλ₯Ό μΈμ©νμΈμ.
|
248 |
+
"""
|
249 |
+
|
250 |
+
# gpt-4.1-mini λͺ¨λΈ μ¬μ©
|
251 |
+
try:
|
252 |
+
response = openai_client.chat.completions.create(
|
253 |
+
model="gpt-4.1-mini",
|
254 |
+
messages=[
|
255 |
+
{"role": "system", "content": system_prompt},
|
256 |
+
{"role": "user", "content": f"λ€μ PDF λ΄μ©μ μ°Έκ³ νμ¬ μ§λ¬Έμ λ΅λ³ν΄μ£ΌμΈμ.\n\nPDF λ΄μ©:\n{all_text}\n\nμ§λ¬Έ: {query}"}
|
257 |
+
],
|
258 |
+
temperature=0.7,
|
259 |
+
max_tokens=2048
|
260 |
+
)
|
261 |
+
|
262 |
+
answer = response.choices[0].message.content
|
263 |
+
return {
|
264 |
+
"answer": answer,
|
265 |
+
"pdf_id": pdf_id,
|
266 |
+
"query": query
|
267 |
+
}
|
268 |
+
except Exception as api_error:
|
269 |
+
logger.error(f"OpenAI API νΈμΆ μ€λ₯: {api_error}")
|
270 |
+
return {"error": f"AI μλ΅ μμ± μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(api_error)}"}
|
271 |
+
|
272 |
+
except Exception as e:
|
273 |
+
logger.error(f"μ§μμλ΅ μ²λ¦¬ μ€λ₯: {e}")
|
274 |
+
return {"error": str(e)}
|
275 |
+
|
276 |
+
# PDF μμ½ μμ±
|
277 |
+
async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
|
278 |
+
try:
|
279 |
+
# μλ² λ© λ°μ΄ν° κ°μ Έμ€κΈ°
|
280 |
+
embedding_data = await get_pdf_embedding(pdf_id)
|
281 |
+
if "error" in embedding_data:
|
282 |
+
return {"error": embedding_data["error"]}
|
283 |
+
|
284 |
+
# μ²ν¬ ν
μ€νΈ λͺ¨μΌκΈ° (μ νλ κΈΈμ΄)
|
285 |
+
all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
|
286 |
+
|
287 |
+
# 컨ν
μ€νΈ ν¬κΈ°λ₯Ό κ³ λ €νμ¬ ν
μ€νΈκ° λ무 κΈΈλ©΄ μλΆλΆλ§ μ¬μ©
|
288 |
+
max_context_length = 60000 # ν ν° μκ° μλ λ¬Έμ μ κΈ°μ€ (λλ΅μ μΈ μ ν)
|
289 |
+
if len(all_text) > max_context_length:
|
290 |
+
all_text = all_text[:max_context_length] + "...(μ΄ν μλ΅)"
|
291 |
+
|
292 |
+
# OpenAI API νΈμΆ
|
293 |
+
try:
|
294 |
+
response = openai_client.chat.completions.create(
|
295 |
+
model="gpt-4.1-mini",
|
296 |
+
messages=[
|
297 |
+
{"role": "system", "content": "λ€μ PDF λ΄μ©μ κ°κ²°νκ² μμ½ν΄μ£ΌμΈμ. ν΅μ¬ μ£Όμ μ μ£Όμ ν¬μΈνΈλ₯Ό ν¬ν¨ν μμ½μ 500μ μ΄λ΄λ‘ μμ±ν΄μ£ΌμΈμ."},
|
298 |
+
{"role": "user", "content": f"PDF λ΄μ©:\n{all_text}"}
|
299 |
+
],
|
300 |
+
temperature=0.7,
|
301 |
+
max_tokens=1024
|
302 |
+
)
|
303 |
+
|
304 |
+
summary = response.choices[0].message.content
|
305 |
+
return {
|
306 |
+
"summary": summary,
|
307 |
+
"pdf_id": pdf_id
|
308 |
+
}
|
309 |
+
except Exception as api_error:
|
310 |
+
logger.error(f"OpenAI API νΈμΆ μ€λ₯: {api_error}")
|
311 |
+
return {"error": f"AI μμ½ μμ± μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(api_error)}"}
|
312 |
+
|
313 |
+
except Exception as e:
|
314 |
+
logger.error(f"PDF μμ½ μμ± μ€λ₯: {e}")
|
315 |
+
return {"error": str(e)}
|
316 |
+
|
317 |
# μ΅μ νλ PDF νμ΄μ§ μΊμ± ν¨μ
|
318 |
async def cache_pdf(pdf_path: str):
|
319 |
try:
|
|
|
468 |
pdf_cache[pdf_name]["status"] = "error"
|
469 |
pdf_cache[pdf_name]["error"] = str(e)
|
470 |
|
|
|
471 |
# PDF IDλ‘ PDF κ²½λ‘ μ°ΎκΈ° (κ°μ λ κ²μ λ‘μ§)
|
472 |
def get_pdf_path_by_id(pdf_id: str) -> str:
|
473 |
logger.info(f"PDF IDλ‘ νμΌ μ‘°ν: {pdf_id}")
|
|
|
701 |
return {name: {"status": info["status"], "progress": info.get("progress", 0)}
|
702 |
for name, info in pdf_cache.items()}
|
703 |
|
704 |
+
# API μλν¬μΈνΈ: PDFμ λν μ§μμλ΅
|
705 |
+
@app.post("/api/ai/query-pdf/{pdf_id}")
|
706 |
+
async def api_query_pdf(pdf_id: str, query: Dict[str, str]):
|
707 |
+
try:
|
708 |
+
user_query = query.get("query", "")
|
709 |
+
if not user_query:
|
710 |
+
return JSONResponse(content={"error": "μ§λ¬Έμ΄ μ 곡λμ§ μμμ΅λλ€"}, status_code=400)
|
711 |
+
|
712 |
+
# PDF κ²½λ‘ νμΈ
|
713 |
+
pdf_path = get_pdf_path_by_id(pdf_id)
|
714 |
+
if not pdf_path:
|
715 |
+
return JSONResponse(content={"error": f"PDF ID {pdf_id}μ ν΄λΉνλ νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
716 |
+
|
717 |
+
# μ§μμλ΅ μ²λ¦¬
|
718 |
+
result = await query_pdf(pdf_id, user_query)
|
719 |
+
|
720 |
+
if "error" in result:
|
721 |
+
return JSONResponse(content={"error": result["error"]}, status_code=500)
|
722 |
+
|
723 |
+
return result
|
724 |
+
except Exception as e:
|
725 |
+
logger.error(f"μ§μμλ΅ API μ€λ₯: {e}")
|
726 |
+
return JSONResponse(content={"error": str(e)}, status_code=500)
|
727 |
+
|
728 |
+
# API μλν¬μΈνΈ: PDF μμ½
|
729 |
+
@app.get("/api/ai/summarize-pdf/{pdf_id}")
|
730 |
+
async def api_summarize_pdf(pdf_id: str):
|
731 |
+
try:
|
732 |
+
# PDF κ²½λ‘ νμΈ
|
733 |
+
pdf_path = get_pdf_path_by_id(pdf_id)
|
734 |
+
if not pdf_path:
|
735 |
+
return JSONResponse(content={"error": f"PDF ID {pdf_id}μ ν΄λΉνλ νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
736 |
+
|
737 |
+
# μμ½ μ²λ¦¬
|
738 |
+
result = await summarize_pdf(pdf_id)
|
739 |
+
|
740 |
+
if "error" in result:
|
741 |
+
return JSONResponse(content={"error": result["error"]}, status_code=500)
|
742 |
+
|
743 |
+
return result
|
744 |
+
except Exception as e:
|
745 |
+
logger.error(f"PDF μμ½ API μ€λ₯: {e}")
|
746 |
+
return JSONResponse(content={"error": str(e)}, status_code=500)
|
747 |
+
|
748 |
# API μλν¬μΈνΈ: μΊμλ PDF μ½ν
μΈ μ 곡 (μ μ§μ λ‘λ© μ§μ)
|
749 |
@app.get("/api/cached-pdf")
|
750 |
async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
|
|
|
1029 |
return RedirectResponse(url=f"/view/{pdf_id}")
|
1030 |
return get_html_content()
|
1031 |
|
1032 |
+
# HTML λ¬Έμμ΄ (AI λ²νΌ λ° μ±λ΄ UI μΆκ°)
|
1033 |
HTML = """
|
1034 |
<!doctype html>
|
1035 |
<html lang="ko">
|
|
|
1055 |
--secondary-color: #ffd6e0; /* νμ€ν
νν¬ */
|
1056 |
--tertiary-color: #c3fae8; /* νμ€ν
λ―ΌνΈ */
|
1057 |
--accent-color: #d0bfff; /* νμ€ν
νΌν */
|
1058 |
+
--ai-color: #86e8ab; /* AI λ²νΌ μμ */
|
1059 |
+
--ai-hover: #65d68a; /* AI νΈλ² μμ */
|
1060 |
--bg-color: #f8f9fa; /* λ°μ λ°°κ²½ */
|
1061 |
--text-color: #495057; /* λΆλλ¬μ΄ μ΄λμ΄ μ */
|
1062 |
--card-bg: #ffffff; /* μΉ΄λ λ°°κ²½μ */
|
|
|
1086 |
}
|
1087 |
|
1088 |
/* ν€λ μ λͺ© μ κ±° λ° Home λ²νΌ λ μ΄μ΄ μ²λ¦¬ */
|
1089 |
+
.floating-home, .floating-ai {
|
1090 |
position: fixed;
|
1091 |
top: 20px;
|
1092 |
left: 20px;
|
|
|
1105 |
overflow: hidden;
|
1106 |
}
|
1107 |
|
1108 |
+
.floating-ai {
|
1109 |
+
top: 90px; /* Home λ²νΌ μλμ μμΉ */
|
1110 |
+
background: rgba(134, 232, 171, 0.9); /* AI λ²νΌ μμ */
|
1111 |
+
}
|
1112 |
+
|
1113 |
+
.floating-home:hover, .floating-ai:hover {
|
1114 |
transform: scale(1.05);
|
1115 |
box-shadow: var(--shadow-lg);
|
1116 |
}
|
1117 |
|
1118 |
+
.floating-home .icon, .floating-ai .icon {
|
1119 |
display: flex;
|
1120 |
justify-content: center;
|
1121 |
align-items: center;
|
|
|
1126 |
transition: var(--transition);
|
1127 |
}
|
1128 |
|
1129 |
+
.floating-ai .icon {
|
1130 |
+
color: white;
|
1131 |
+
}
|
1132 |
+
|
1133 |
.floating-home:hover .icon {
|
1134 |
color: #8bc5f8;
|
1135 |
}
|
1136 |
|
1137 |
+
.floating-ai:hover .icon {
|
1138 |
+
color: #ffffff;
|
1139 |
+
}
|
1140 |
+
|
1141 |
+
.floating-home .title, .floating-ai .title {
|
1142 |
position: absolute;
|
1143 |
left: 70px;
|
1144 |
background: rgba(255, 255, 255, 0.95);
|
|
|
1154 |
transition: all 0.3s ease;
|
1155 |
}
|
1156 |
|
1157 |
+
.floating-home:hover .title, .floating-ai:hover .title {
|
1158 |
opacity: 1;
|
1159 |
transform: translateX(0);
|
1160 |
}
|
|
|
1698 |
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
1699 |
}
|
1700 |
|
1701 |
+
/* AI μ±λ΄ UI μ€νμΌ */
|
1702 |
+
#aiChatContainer {
|
1703 |
+
display: none;
|
1704 |
+
position: fixed;
|
1705 |
+
top: 0;
|
1706 |
+
right: 0;
|
1707 |
+
width: 400px;
|
1708 |
+
height: 100%;
|
1709 |
+
background: rgba(255, 255, 255, 0.95);
|
1710 |
+
backdrop-filter: blur(10px);
|
1711 |
+
box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1);
|
1712 |
+
z-index: 9999;
|
1713 |
+
transition: all 0.3s ease;
|
1714 |
+
transform: translateX(100%);
|
1715 |
+
padding: 20px;
|
1716 |
+
box-sizing: border-box;
|
1717 |
+
display: flex;
|
1718 |
+
flex-direction: column;
|
1719 |
+
}
|
1720 |
+
|
1721 |
+
#aiChatContainer.active {
|
1722 |
+
transform: translateX(0);
|
1723 |
+
}
|
1724 |
+
|
1725 |
+
#aiChatHeader {
|
1726 |
+
display: flex;
|
1727 |
+
justify-content: space-between;
|
1728 |
+
align-items: center;
|
1729 |
+
margin-bottom: 15px;
|
1730 |
+
padding-bottom: 15px;
|
1731 |
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
1732 |
+
}
|
1733 |
+
|
1734 |
+
#aiChatHeader h3 {
|
1735 |
+
margin: 0;
|
1736 |
+
color: #333;
|
1737 |
+
font-size: 18px;
|
1738 |
+
display: flex;
|
1739 |
+
align-items: center;
|
1740 |
+
}
|
1741 |
+
|
1742 |
+
#aiChatHeader h3 i {
|
1743 |
+
margin-right: 10px;
|
1744 |
+
color: var(--ai-color);
|
1745 |
+
}
|
1746 |
+
|
1747 |
+
#aiChatClose {
|
1748 |
+
background: none;
|
1749 |
+
border: none;
|
1750 |
+
cursor: pointer;
|
1751 |
+
font-size: 18px;
|
1752 |
+
color: #666;
|
1753 |
+
transition: var(--transition);
|
1754 |
+
}
|
1755 |
+
|
1756 |
+
#aiChatClose:hover {
|
1757 |
+
color: #333;
|
1758 |
+
transform: scale(1.1);
|
1759 |
+
}
|
1760 |
+
|
1761 |
+
#aiChatMessages {
|
1762 |
+
flex: 1;
|
1763 |
+
overflow-y: auto;
|
1764 |
+
padding: 10px 0;
|
1765 |
+
margin-bottom: 15px;
|
1766 |
+
}
|
1767 |
+
|
1768 |
+
.chat-message {
|
1769 |
+
margin-bottom: 15px;
|
1770 |
+
display: flex;
|
1771 |
+
align-items: flex-start;
|
1772 |
+
}
|
1773 |
+
|
1774 |
+
.chat-message.user {
|
1775 |
+
flex-direction: row-reverse;
|
1776 |
+
}
|
1777 |
+
|
1778 |
+
.chat-avatar {
|
1779 |
+
width: 35px;
|
1780 |
+
height: 35px;
|
1781 |
+
border-radius: 50%;
|
1782 |
+
display: flex;
|
1783 |
+
justify-content: center;
|
1784 |
+
align-items: center;
|
1785 |
+
margin-right: 10px;
|
1786 |
+
flex-shrink: 0;
|
1787 |
+
}
|
1788 |
+
|
1789 |
+
.chat-message.user .chat-avatar {
|
1790 |
+
margin-right: 0;
|
1791 |
+
margin-left: 10px;
|
1792 |
+
background: var(--primary-color);
|
1793 |
+
color: white;
|
1794 |
+
}
|
1795 |
+
|
1796 |
+
.chat-message.ai .chat-avatar {
|
1797 |
+
background: var(--ai-color);
|
1798 |
+
color: white;
|
1799 |
+
}
|
1800 |
+
|
1801 |
+
.chat-content {
|
1802 |
+
background: #f1f1f1;
|
1803 |
+
padding: 12px 15px;
|
1804 |
+
border-radius: 18px;
|
1805 |
+
max-width: 75%;
|
1806 |
+
word-break: break-word;
|
1807 |
+
position: relative;
|
1808 |
+
font-size: 14px;
|
1809 |
+
line-height: 1.4;
|
1810 |
+
}
|
1811 |
+
|
1812 |
+
.chat-message.user .chat-content {
|
1813 |
+
background: var(--primary-color);
|
1814 |
+
color: white;
|
1815 |
+
border-bottom-right-radius: 4px;
|
1816 |
+
}
|
1817 |
+
|
1818 |
+
.chat-message.ai .chat-content {
|
1819 |
+
background: #f1f1f1;
|
1820 |
+
color: #333;
|
1821 |
+
border-bottom-left-radius: 4px;
|
1822 |
+
}
|
1823 |
+
|
1824 |
+
#aiChatForm {
|
1825 |
+
display: flex;
|
1826 |
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
1827 |
+
padding-top: 15px;
|
1828 |
+
}
|
1829 |
+
|
1830 |
+
#aiChatInput {
|
1831 |
+
flex: 1;
|
1832 |
+
padding: 12px 15px;
|
1833 |
+
border: 1px solid #ddd;
|
1834 |
+
border-radius: 25px;
|
1835 |
+
font-size: 14px;
|
1836 |
+
outline: none;
|
1837 |
+
transition: var(--transition);
|
1838 |
+
}
|
1839 |
+
|
1840 |
+
#aiChatInput:focus {
|
1841 |
+
border-color: var(--ai-color);
|
1842 |
+
box-shadow: 0 0 0 2px rgba(134, 232, 171, 0.2);
|
1843 |
+
}
|
1844 |
+
|
1845 |
+
#aiChatSubmit {
|
1846 |
+
background: var(--ai-color);
|
1847 |
+
border: none;
|
1848 |
+
color: white;
|
1849 |
+
width: 45px;
|
1850 |
+
height: 45px;
|
1851 |
+
border-radius: 50%;
|
1852 |
+
margin-left: 10px;
|
1853 |
+
display: flex;
|
1854 |
+
justify-content: center;
|
1855 |
+
align-items: center;
|
1856 |
+
cursor: pointer;
|
1857 |
+
transition: var(--transition);
|
1858 |
+
}
|
1859 |
+
|
1860 |
+
#aiChatSubmit:hover {
|
1861 |
+
background: var(--ai-hover);
|
1862 |
+
transform: scale(1.05);
|
1863 |
+
}
|
1864 |
+
|
1865 |
+
#aiChatSubmit:disabled {
|
1866 |
+
background: #ccc;
|
1867 |
+
cursor: not-allowed;
|
1868 |
+
}
|
1869 |
+
|
1870 |
+
.typing-indicator {
|
1871 |
+
display: flex;
|
1872 |
+
align-items: center;
|
1873 |
+
margin-top: 5px;
|
1874 |
+
font-size: 12px;
|
1875 |
+
color: #666;
|
1876 |
+
}
|
1877 |
+
|
1878 |
+
.typing-indicator span {
|
1879 |
+
height: 8px;
|
1880 |
+
width: 8px;
|
1881 |
+
background: var(--ai-color);
|
1882 |
+
border-radius: 50%;
|
1883 |
+
display: inline-block;
|
1884 |
+
margin-right: 3px;
|
1885 |
+
animation: typing 1s infinite;
|
1886 |
+
}
|
1887 |
+
|
1888 |
+
.typing-indicator span:nth-child(2) {
|
1889 |
+
animation-delay: 0.2s;
|
1890 |
+
}
|
1891 |
+
|
1892 |
+
.typing-indicator span:nth-child(3) {
|
1893 |
+
animation-delay: 0.4s;
|
1894 |
+
}
|
1895 |
+
|
1896 |
+
@keyframes typing {
|
1897 |
+
0% { transform: translateY(0); }
|
1898 |
+
50% { transform: translateY(-5px); }
|
1899 |
+
100% { transform: translateY(0); }
|
1900 |
+
}
|
1901 |
+
|
1902 |
+
.chat-time {
|
1903 |
+
font-size: 10px;
|
1904 |
+
color: #999;
|
1905 |
+
margin-top: 5px;
|
1906 |
+
text-align: right;
|
1907 |
+
}
|
1908 |
+
|
1909 |
+
/* μ½λ λΈλ‘ μ€νμΌ */
|
1910 |
+
.chat-content pre {
|
1911 |
+
background: rgba(0, 0, 0, 0.05);
|
1912 |
+
padding: 10px;
|
1913 |
+
border-radius: 5px;
|
1914 |
+
overflow-x: auto;
|
1915 |
+
font-family: monospace;
|
1916 |
+
font-size: 12px;
|
1917 |
+
margin: 10px 0;
|
1918 |
+
}
|
1919 |
+
|
1920 |
+
/* λ§ν¬λ€μ΄ μ€νμΌ */
|
1921 |
+
.chat-content strong {
|
1922 |
+
font-weight: bold;
|
1923 |
+
}
|
1924 |
+
|
1925 |
+
.chat-content em {
|
1926 |
+
font-style: italic;
|
1927 |
+
}
|
1928 |
+
|
1929 |
+
.chat-content ul, .chat-content ol {
|
1930 |
+
margin-left: 20px;
|
1931 |
+
margin-top: 5px;
|
1932 |
+
margin-bottom: 5px;
|
1933 |
+
}
|
1934 |
+
|
1935 |
+
/* 곡μ λ²νΌ */
|
1936 |
+
#shareChat {
|
1937 |
+
padding: 8px 15px;
|
1938 |
+
background: #f1f1f1;
|
1939 |
+
border: none;
|
1940 |
+
border-radius: 20px;
|
1941 |
+
font-size: 12px;
|
1942 |
+
color: #666;
|
1943 |
+
cursor: pointer;
|
1944 |
+
margin-top: 5px;
|
1945 |
+
transition: var(--transition);
|
1946 |
+
}
|
1947 |
+
|
1948 |
+
#shareChat:hover {
|
1949 |
+
background: #ddd;
|
1950 |
+
}
|
1951 |
+
|
1952 |
/* λ°μν λμμΈ */
|
1953 |
@media (max-width: 768px) {
|
1954 |
.grid {
|
|
|
1965 |
padding: 10px 20px;
|
1966 |
}
|
1967 |
|
1968 |
+
.floating-home, .floating-ai {
|
1969 |
width: 50px;
|
1970 |
height: 50px;
|
1971 |
}
|
1972 |
|
1973 |
+
.floating-home .icon, .floating-ai .icon {
|
1974 |
font-size: 18px;
|
1975 |
}
|
1976 |
|
|
|
1978 |
padding: 6px 15px;
|
1979 |
font-size: 12px;
|
1980 |
}
|
1981 |
+
|
1982 |
+
#aiChatContainer {
|
1983 |
+
width: 100%;
|
1984 |
+
}
|
1985 |
}
|
1986 |
</style>
|
1987 |
</head>
|
|
|
1992 |
<div class="title">νμΌλ‘ λμκ°κΈ°</div>
|
1993 |
</div>
|
1994 |
|
1995 |
+
<!-- AI λ²νΌ μΆκ° -->
|
1996 |
+
<div id="aiButton" class="floating-ai" style="display:none;">
|
1997 |
+
<div class="icon"><i class="fas fa-robot"></i></div>
|
1998 |
+
<div class="title">AI μ΄μμ€ν΄νΈ</div>
|
1999 |
+
</div>
|
2000 |
+
|
2001 |
+
<!-- AI μ±λ΄ 컨ν
μ΄λ -->
|
2002 |
+
<div id="aiChatContainer">
|
2003 |
+
<div id="aiChatHeader">
|
2004 |
+
<h3><i class="fas fa-robot"></i> AI μ΄μμ€ν΄νΈ</h3>
|
2005 |
+
<button id="aiChatClose"><i class="fas fa-times"></i></button>
|
2006 |
+
</div>
|
2007 |
+
<div id="aiChatMessages"></div>
|
2008 |
+
<form id="aiChatForm">
|
2009 |
+
<input type="text" id="aiChatInput" placeholder="PDFμ λν΄ μ§λ¬ΈνμΈμ..." autocomplete="off">
|
2010 |
+
<button type="submit" id="aiChatSubmit"><i class="fas fa-paper-plane"></i></button>
|
2011 |
+
</form>
|
2012 |
+
</div>
|
2013 |
+
|
2014 |
<!-- κ΄λ¦¬μ λ²νΌ -->
|
2015 |
<div id="adminButton">
|
2016 |
<i class="fas fa-cog"></i> Admin
|
|
|
2088 |
let audioInitialized = false;
|
2089 |
let audioContext = null;
|
2090 |
|
2091 |
+
// AI μ±λ΄ κ΄λ ¨ λ³μ
|
2092 |
+
let isAiChatActive = false;
|
2093 |
+
let isAiProcessing = false;
|
2094 |
+
let hasLoadedSummary = false;
|
2095 |
+
|
2096 |
// μ€λμ€ μ΄κΈ°ν ν¨μ
|
2097 |
function initializeAudio() {
|
2098 |
if (audioInitialized) return Promise.resolve();
|
|
|
2190 |
/* ββ μ νΈ ββ */
|
2191 |
function $id(id){return document.getElementById(id)}
|
2192 |
|
2193 |
+
// νμ¬ μκ°μ ν¬λ§·ν
νλ ν¨μ
|
2194 |
+
function formatTime() {
|
2195 |
+
const now = new Date();
|
2196 |
+
const hours = now.getHours().toString().padStart(2, '0');
|
2197 |
+
const minutes = now.getMinutes().toString().padStart(2, '0');
|
2198 |
+
return `${hours}:${minutes}`;
|
2199 |
+
}
|
2200 |
+
|
2201 |
+
// AI μ±λ΄ λ©μμ§ μΆκ° ν¨μ
|
2202 |
+
function addChatMessage(content, isUser = false) {
|
2203 |
+
const messagesContainer = $id('aiChatMessages');
|
2204 |
+
const messageElement = document.createElement('div');
|
2205 |
+
messageElement.className = `chat-message ${isUser ? 'user' : 'ai'}`;
|
2206 |
+
|
2207 |
+
const currentTime = formatTime();
|
2208 |
+
|
2209 |
+
messageElement.innerHTML = `
|
2210 |
+
<div class="chat-avatar">
|
2211 |
+
<i class="fas ${isUser ? 'fa-user' : 'fa-robot'}"></i>
|
2212 |
+
</div>
|
2213 |
+
<div class="chat-bubble">
|
2214 |
+
<div class="chat-content">${content}</div>
|
2215 |
+
<div class="chat-time">${currentTime}</div>
|
2216 |
+
</div>
|
2217 |
+
`;
|
2218 |
+
|
2219 |
+
messagesContainer.appendChild(messageElement);
|
2220 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
2221 |
+
return messageElement;
|
2222 |
+
}
|
2223 |
+
|
2224 |
+
// λ‘λ© νμκΈ° μΆκ° ν¨μ
|
2225 |
+
function addTypingIndicator() {
|
2226 |
+
const messagesContainer = $id('aiChatMessages');
|
2227 |
+
const indicatorElement = document.createElement('div');
|
2228 |
+
indicatorElement.className = 'typing-indicator';
|
2229 |
+
indicatorElement.innerHTML = `
|
2230 |
+
<div class="chat-avatar">
|
2231 |
+
<i class="fas fa-robot"></i>
|
2232 |
+
</div>
|
2233 |
+
<div>
|
2234 |
+
<span></span>
|
2235 |
+
<span></span>
|
2236 |
+
<span></span>
|
2237 |
+
</div>
|
2238 |
+
`;
|
2239 |
+
messagesContainer.appendChild(indicatorElement);
|
2240 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
2241 |
+
return indicatorElement;
|
2242 |
+
}
|
2243 |
+
|
2244 |
+
// AI μ±λ΄ ν κΈ ν¨μ
|
2245 |
+
function toggleAiChat(show = true) {
|
2246 |
+
const aiChatContainer = $id('aiChatContainer');
|
2247 |
+
|
2248 |
+
if (show) {
|
2249 |
+
// μ±λ΄ νμ
|
2250 |
+
aiChatContainer.style.display = 'flex';
|
2251 |
+
setTimeout(() => {
|
2252 |
+
aiChatContainer.classList.add('active');
|
2253 |
+
}, 10);
|
2254 |
+
isAiChatActive = true;
|
2255 |
+
|
2256 |
+
// μ²μ μ΄ λ μλ μμ½ λ‘λ
|
2257 |
+
if (!hasLoadedSummary && currentPdfId) {
|
2258 |
+
loadPdfSummary();
|
2259 |
+
}
|
2260 |
+
} else {
|
2261 |
+
// μ±λ΄ μ¨κΈ°κΈ°
|
2262 |
+
aiChatContainer.classList.remove('active');
|
2263 |
+
setTimeout(() => {
|
2264 |
+
aiChatContainer.style.display = 'none';
|
2265 |
+
}, 300);
|
2266 |
+
isAiChatActive = false;
|
2267 |
+
}
|
2268 |
+
}
|
2269 |
+
|
2270 |
+
// PDF μμ½ λ‘λ ν¨μ
|
2271 |
+
async function loadPdfSummary() {
|
2272 |
+
if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
|
2273 |
+
|
2274 |
+
try {
|
2275 |
+
isAiProcessing = true;
|
2276 |
+
const typingIndicator = addTypingIndicator();
|
2277 |
+
|
2278 |
+
// μλ²μ μμ½ μμ²
|
2279 |
+
const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
|
2280 |
+
const data = await response.json();
|
2281 |
+
|
2282 |
+
// λ‘λ© νμκΈ° μ κ±°
|
2283 |
+
typingIndicator.remove();
|
2284 |
+
|
2285 |
+
if (data.error) {
|
2286 |
+
addChatMessage(`μμ½μ μμ±νλ μ€ μ€λ₯κ° λ°μνμ΅λλ€: ${data.error}`);
|
2287 |
+
} else {
|
2288 |
+
// νμ λ©μμ§μ μμ½ μΆκ°
|
2289 |
+
addChatMessage(`μλ
νμΈμ! μ΄ PDFμ λν΄ μ΄λ€ κ²μ΄λ μ§λ¬Έν΄μ£ΌμΈμ. μ κ° λμλλ¦¬κ² μ΅λλ€.<br><br><strong>PDF μμ½:</strong><br>${data.summary}`);
|
2290 |
+
hasLoadedSummary = true;
|
2291 |
+
}
|
2292 |
+
} catch (error) {
|
2293 |
+
console.error("PDF μμ½ λ‘λ μ€λ₯:", error);
|
2294 |
+
addChatMessage("PDF μμ½μ λ‘λνλ μ€ μ€λ₯κ° λ°μνμ΅λλ€. μ μ ν λ€μ μλν΄μ£ΌμΈμ.");
|
2295 |
+
} finally {
|
2296 |
+
isAiProcessing = false;
|
2297 |
+
}
|
2298 |
+
}
|
2299 |
+
|
2300 |
+
// μ§λ¬Έ μ μΆ ν¨μ
|
2301 |
+
async function submitQuestion(question) {
|
2302 |
+
if (!currentPdfId || isAiProcessing || !question.trim()) return;
|
2303 |
+
|
2304 |
+
try {
|
2305 |
+
isAiProcessing = true;
|
2306 |
+
$id('aiChatSubmit').disabled = true;
|
2307 |
+
|
2308 |
+
// μ¬μ©μ λ©μμ§ μΆκ°
|
2309 |
+
addChatMessage(question, true);
|
2310 |
+
|
2311 |
+
// λ‘λ© νμκΈ° μΆκ°
|
2312 |
+
const typingIndicator = addTypingIndicator();
|
2313 |
+
|
2314 |
+
// μλ²μ μ§μ μμ²
|
2315 |
+
const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
|
2316 |
+
method: 'POST',
|
2317 |
+
headers: {
|
2318 |
+
'Content-Type': 'application/json'
|
2319 |
+
},
|
2320 |
+
body: JSON.stringify({ query: question })
|
2321 |
+
});
|
2322 |
+
|
2323 |
+
const data = await response.json();
|
2324 |
+
|
2325 |
+
// λ‘λ© νμκΈ° μ κ±°
|
2326 |
+
typingIndicator.remove();
|
2327 |
+
|
2328 |
+
if (data.error) {
|
2329 |
+
addChatMessage(`μ£μ‘ν©λλ€. μ§λ¬Έμ λ΅λ³νλ μ€ μ€λ₯κ° λ°μνμ΅λλ€: ${data.error}`);
|
2330 |
+
} else {
|
2331 |
+
// AI μλ΅ μΆκ° (λ§ν¬λ€μ΄ μ²λ¦¬ λ± νμμ μΆκ°)
|
2332 |
+
addChatMessage(data.answer);
|
2333 |
+
}
|
2334 |
+
} catch (error) {
|
2335 |
+
console.error("μ§λ¬Έ μ μΆ μ€λ₯:", error);
|
2336 |
+
addChatMessage("μ£μ‘ν©λλ€. μλ²μ ν΅μ μ€ μ€λ₯κ° λ°μνμ΅λοΏ½οΏ½. μ μ ν λ€μ μλν΄μ£ΌμΈμ.");
|
2337 |
+
} finally {
|
2338 |
+
isAiProcessing = false;
|
2339 |
+
$id('aiChatSubmit').disabled = false;
|
2340 |
+
$id('aiChatInput').value = '';
|
2341 |
+
$id('aiChatInput').focus();
|
2342 |
+
}
|
2343 |
+
}
|
2344 |
+
|
2345 |
// DOMμ΄ λ‘λλλ©΄ μ€ν
|
2346 |
document.addEventListener('DOMContentLoaded', function() {
|
2347 |
console.log("DOM λ‘λ μλ£, μ΄λ²€νΈ μ€μ μμ");
|
|
|
2403 |
$id('loadingPages').style.display = 'none';
|
2404 |
currentLoadingPdfPath = null;
|
2405 |
currentPdfId = null;
|
2406 |
+
|
2407 |
+
// AI μ±λ΄ λ«κΈ°
|
2408 |
+
toggleAiChat(false);
|
2409 |
+
hasLoadedSummary = false; // μμ½ λ‘λ μν μ΄κΈ°ν
|
2410 |
+
});
|
2411 |
+
}
|
2412 |
+
|
2413 |
+
// AI λ²νΌ μ΄λ²€νΈ μ€μ
|
2414 |
+
const aiButton = document.getElementById('aiButton');
|
2415 |
+
if (aiButton) {
|
2416 |
+
aiButton.addEventListener('click', function() {
|
2417 |
+
toggleAiChat(!isAiChatActive);
|
2418 |
+
});
|
2419 |
+
}
|
2420 |
+
|
2421 |
+
// AI μ±λ΄ λ«κΈ° λ²νΌ
|
2422 |
+
const aiChatClose = document.getElementById('aiChatClose');
|
2423 |
+
if (aiChatClose) {
|
2424 |
+
aiChatClose.addEventListener('click', function() {
|
2425 |
+
toggleAiChat(false);
|
2426 |
+
});
|
2427 |
+
}
|
2428 |
+
|
2429 |
+
// AI μ±λ΄ νΌ μ μΆ
|
2430 |
+
const aiChatForm = document.getElementById('aiChatForm');
|
2431 |
+
if (aiChatForm) {
|
2432 |
+
aiChatForm.addEventListener('submit', function(e) {
|
2433 |
+
e.preventDefault();
|
2434 |
+
const inputField = document.getElementById('aiChatInput');
|
2435 |
+
const question = inputField.value.trim();
|
2436 |
+
|
2437 |
+
if (question && !isAiProcessing) {
|
2438 |
+
submitQuestion(question);
|
2439 |
+
}
|
2440 |
});
|
2441 |
}
|
2442 |
});
|
|
|
2706 |
createFlipBook(cachedData.pages);
|
2707 |
// νμ¬ μ΄λ¦° PDFμ ID μ μ₯
|
2708 |
currentPdfId = pdfId;
|
2709 |
+
// AI λ²νΌ νμ
|
2710 |
+
$id('aiButton').style.display = 'block';
|
2711 |
return;
|
2712 |
}
|
2713 |
} catch (error) {
|
|
|
2747 |
|
2748 |
// νμ¬ μ΄λ¦° PDFμ ID μ μ₯
|
2749 |
currentPdfId = pdfId;
|
2750 |
+
// AI λ²νΌ νμ
|
2751 |
+
$id('aiButton').style.display = 'block';
|
2752 |
}
|
2753 |
} catch (error) {
|
2754 |
console.error("PDF IDλ‘ μ΄κΈ° μ€ν¨:", error);
|
|
|
2787 |
const card = document.querySelectorAll('.card')[i];
|
2788 |
if (card && card.dataset.pdfId) {
|
2789 |
currentPdfId = card.dataset.pdfId;
|
2790 |
+
// AI λ²νΌ νμ
|
2791 |
+
$id('aiButton').style.display = 'block';
|
2792 |
} else {
|
2793 |
currentPdfId = null;
|
2794 |
+
// AI λ²νΌ μ¨κΉ
|
2795 |
+
$id('aiButton').style.display = 'none';
|
2796 |
}
|
2797 |
|
2798 |
+
// AI μ±λ΄ μ΄κΈ°ν
|
2799 |
+
toggleAiChat(false);
|
2800 |
+
hasLoadedSummary = false;
|
2801 |
+
$id('aiChatMessages').innerHTML = '';
|
2802 |
+
|
2803 |
// κΈ°μ‘΄ FlipBook μ 리
|
2804 |
if(fb) {
|
2805 |
fb.destroy();
|
|
|
3134 |
$id('homeButton').style.display=showHome?'none':'block';
|
3135 |
$id('adminPage').style.display='none';
|
3136 |
|
3137 |
+
// AI λ²νΌ κ΄λ¦¬
|
3138 |
+
$id('aiButton').style.display = (!showHome && currentPdfId) ? 'block' : 'none';
|
3139 |
+
|
3140 |
+
// AI μ±λ΄μ΄ μ΄λ €μμΌλ©΄ λ«κΈ°
|
3141 |
+
if (isAiChatActive) {
|
3142 |
+
toggleAiChat(false);
|
3143 |
+
}
|
3144 |
+
|
3145 |
// λ·°μ΄ λͺ¨λμΌ λ μ€νμΌ λ³κ²½
|
3146 |
if(!showHome) {
|
3147 |
document.body.classList.add('viewer-mode');
|