Spaces:
Paused
Paused
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');
|