hwp-agent / index.html
ginipick's picture
Create index.html
6112d2b verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ν•œμ§€ HANJI Β· On-Premise HWP AI Agent</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box;}
:root{
--bg:#ffffff;--bg2:#f8f9fb;--bg3:#f1f3f6;
--text:#1a1a2e;--text2:#475569;--muted:#94a3b8;
--border:#e8ecf1;--border2:#d1d5db;
--accent:#4f46e5;--accent2:#6366f1;--accent-light:rgba(99,102,241,.08);
--teal:#0d9488;--teal-light:rgba(13,148,136,.08);
--amber:#d97706;--red:#dc2626;--green:#16a34a;
--r:12px;--r-sm:8px;
--font:'Noto Sans KR',sans-serif;--mono:'JetBrains Mono',monospace;
--shadow:0 4px 16px rgba(0,0,0,.06);--shadow-lg:0 8px 32px rgba(0,0,0,.08);
--tr:.2s ease;
}
html,body{height:100%;overflow:hidden;font-family:var(--font);background:var(--bg);color:var(--text);font-size:13px;-webkit-font-smoothing:antialiased;}
::-webkit-scrollbar{width:5px;height:5px;}
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:10px;}
::selection{background:var(--accent-light);color:var(--accent);}
@keyframes shimmer{0%,100%{background-position:0%}50%{background-position:100%}}
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.topbar{height:46px;display:flex;align-items:center;gap:10px;padding:0 20px;background:var(--bg);border-bottom:1px solid var(--border);}
.logo{font-weight:900;font-size:15px;color:#1e1b4b;letter-spacing:-.3px;}
.logo em{font-style:normal;font-weight:400;font-size:11px;color:var(--text2);margin-left:2px;}
.topbar-sep{width:1px;height:18px;background:var(--border);margin:0 2px;}
.topbar-desc{font-size:11px;color:var(--text2);font-weight:500;}
.topbar-url{display:inline-flex;align-items:center;gap:4px;padding:3px 12px;border-radius:20px;
font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:.3px;
background:linear-gradient(135deg,#4f46e5,#4338ca);color:#fff;text-decoration:none;
box-shadow:0 2px 8px rgba(79,70,229,.2);transition:var(--tr);}
.topbar-url:hover{box-shadow:0 4px 14px rgba(79,70,229,.35);transform:translateY(-1px);}
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;}
.topbar-contact{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;
font-family:var(--mono);font-size:8.5px;font-weight:600;color:var(--teal);
background:var(--teal-light);border:1px solid rgba(13,148,136,.12);
text-decoration:none;transition:var(--tr);}
.topbar-contact:hover{background:rgba(13,148,136,.12);}
.chip{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;
font-family:var(--mono);font-size:8px;font-weight:600;letter-spacing:.8px;
background:var(--accent-light);color:var(--accent);border:1px solid rgba(99,102,241,.12);}
.chip-teal{background:var(--teal-light);color:var(--teal);border-color:rgba(13,148,136,.12);}
.dot-live{width:5px;height:5px;border-radius:50%;background:var(--green);animation:pulse 2s infinite;}
.main{display:grid;grid-template-columns:340px 1fr;height:calc(100vh - 50px);overflow:hidden;}
.left{border-right:1px solid var(--border);overflow-y:auto;padding:16px;background:var(--bg);}
.section{margin-bottom:14px;animation:fadeUp .4s ease both;}
.section:nth-child(2){animation-delay:.05s;}
.section:nth-child(3){animation-delay:.1s;}
.label{font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}
textarea{width:100%;border:1px solid var(--border);border-radius:var(--r-sm);padding:10px 12px;font-family:var(--font);font-size:12.5px;color:var(--text);background:var(--bg);resize:none;outline:none;transition:var(--tr);line-height:1.6;}
textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-light);}
textarea::placeholder{color:var(--muted);font-size:11.5px;}
.file-drop{border:2px dashed var(--border);border-radius:var(--r);padding:18px;text-align:center;cursor:pointer;transition:var(--tr);background:var(--bg2);}
.file-drop:hover{border-color:var(--accent);background:var(--accent-light);}
.file-drop.has-file{border-color:var(--green);background:rgba(22,163,74,.04);}
.file-icon{font-size:22px;margin-bottom:4px;opacity:.4;}
.file-hint{font-size:10px;color:var(--muted);font-family:var(--mono);}
.file-name{font-size:11px;font-weight:600;color:var(--green);margin-top:4px;display:none;}
.slider-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.slider-group{background:var(--bg2);border-radius:var(--r-sm);padding:8px 10px;}
.slider-label{font-family:var(--mono);font-size:8.5px;font-weight:600;color:var(--muted);margin-bottom:4px;display:flex;justify-content:space-between;}
.slider-val{color:var(--accent);font-weight:700;}
input[type=range]{width:100%;height:4px;-webkit-appearance:none;background:var(--border);border-radius:4px;outline:none;}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.15);}
.btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 16px;border-radius:var(--r-sm);font-family:var(--font);font-size:12px;font-weight:600;border:none;cursor:pointer;transition:var(--tr);width:100%;}
.btn-primary{background:linear-gradient(135deg,var(--accent),#4338ca);color:#fff;box-shadow:0 4px 12px rgba(79,70,229,.25);}
.btn-primary:hover{box-shadow:0 6px 20px rgba(79,70,229,.35);transform:translateY(-1px);}
.btn-primary:active{transform:translateY(0);}
.btn-primary:disabled{opacity:.5;cursor:not-allowed;transform:none;}
.btn-secondary{background:var(--bg2);color:var(--text2);border:1px solid var(--border);}
.btn-secondary:hover{background:var(--bg3);}
.btn-sm{padding:6px 10px;font-size:10.5px;border-radius:6px;}
.btn-row{display:grid;grid-template-columns:1fr auto;gap:8px;}
.btn-row-2{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.status-bar{font-family:var(--mono);font-size:10px;color:var(--muted);padding:6px 0;display:flex;align-items:center;gap:6px;}
.dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;}
.status-idle{background:var(--muted);}
.status-run{background:var(--accent);animation:pulse 1.5s infinite;}
.status-done{background:var(--green);}
.status-err{background:var(--red);}
.acc{border:1px solid var(--border);border-radius:var(--r-sm);overflow:hidden;margin-bottom:8px;}
.acc-header{display:flex;align-items:center;gap:6px;padding:8px 12px;cursor:pointer;font-family:var(--mono);font-size:9.5px;font-weight:600;color:var(--text2);background:var(--bg2);transition:var(--tr);user-select:none;}
.acc-header:hover{background:var(--bg3);}
.acc-arrow{font-size:8px;transition:var(--tr);color:var(--muted);}
.acc.open .acc-arrow{transform:rotate(90deg);}
.acc-body{display:none;padding:10px 12px;border-top:1px solid var(--border);background:var(--bg);}
.acc.open .acc-body{display:block;}
.acc textarea{font-size:11px;font-family:var(--mono);line-height:1.6;background:var(--bg2);border:none;}
.dl-area{background:var(--bg2);border-radius:var(--r-sm);padding:10px 12px;}
.dl-fname{font-family:var(--mono);font-size:10px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px;}
.dl-status{font-family:var(--mono);font-size:9px;margin-top:4px;}
.dl-status.ok{color:var(--green);}
.dl-status.err{color:var(--red);}
.right{background:#d8dce2;overflow:hidden;display:flex;flex-direction:column;}
.vt{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--bg);border-bottom:1px solid var(--border);flex-shrink:0;}
.vt-dots{display:flex;gap:5px;}
.vt-dots span{width:10px;height:10px;border-radius:50%;display:block;}
.vt-fname{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--text);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.vt-badge{font-family:var(--mono);font-size:7.5px;font-weight:700;padding:2px 10px;border-radius:20px;letter-spacing:.5px;}
.badge-r{background:var(--teal-light);color:var(--teal);border:1px solid rgba(13,148,136,.15);}
.badge-e{background:var(--accent-light);color:var(--accent);border:1px solid rgba(99,102,241,.12);}
.badge-l{background:rgba(251,191,36,.1);color:var(--amber);border:1px solid rgba(251,191,36,.2);animation:pulse 1.5s infinite;}
.vb{flex:1;overflow:hidden;position:relative;}
.vb iframe{position:absolute;top:0;left:0;width:100%!important;height:100%!important;border:none!important;display:block;}
.ve{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:14px;color:var(--muted);background:#d8dce2;}
.ve-icon{font-size:48px;opacity:.2;}
.ve-text{font-family:var(--mono);font-size:10px;text-align:center;line-height:1.8;}
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(100px);padding:10px 20px;border-radius:var(--r);font-size:11px;font-weight:600;color:#fff;z-index:9999;opacity:0;transition:.3s ease;pointer-events:none;box-shadow:var(--shadow-lg);}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
.toast.ok{background:var(--green);}.toast.err{background:var(--red);}.toast.info{background:var(--accent);}
.spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:none;}
.running .spinner{display:inline-block;}
#fileInput{display:none;}
.mode-group{display:flex;flex-direction:column;gap:2px;}
.mode-opt{display:flex;align-items:flex-start;gap:8px;padding:7px 10px;border-radius:var(--r-sm);cursor:pointer;transition:var(--tr);border:1px solid transparent;}
.mode-opt:hover{background:var(--bg2);}
.mode-opt.active{background:var(--accent-light);border-color:rgba(99,102,241,.15);}
.mode-opt.disabled{opacity:.35;cursor:not-allowed;pointer-events:none;}
.mode-opt input[type=radio]{margin-top:2px;accent-color:var(--accent);flex-shrink:0;}
.mode-opt .mo-text{display:flex;flex-direction:column;gap:1px;}
.mode-opt .mo-label{font-size:11.5px;font-weight:600;color:var(--text);}
.mode-opt .mo-desc{font-size:9.5px;color:var(--muted);line-height:1.4;}
.mode-opt .mo-badge{display:inline-block;font-family:var(--mono);font-size:7.5px;font-weight:700;padding:1px 6px;border-radius:10px;background:rgba(251,191,36,.1);color:var(--amber);border:1px solid rgba(251,191,36,.2);margin-left:4px;vertical-align:middle;}
@media(max-width:900px){
.main{grid-template-columns:1fr;grid-template-rows:auto 1fr;}
.left{max-height:40vh;border-right:none;border-bottom:1px solid var(--border);}
}
</style>
</head>
<body>
<div class="topbar">
<span class="logo">ν•œμ§€<em>(HANJI)</em></span>
<span class="topbar-sep"></span>
<span class="topbar-desc">HWP AI Agent μ„œλΉ„μŠ€</span>
<a class="topbar-url" href="https://hanji.ginigen.ai" target="_blank">πŸ”— hanji.ginigen.ai</a>
<span class="topbar-right">
<a class="topbar-contact" href="/cdn-cgi/l/email-protection#7017191e1917151e1119180030171d11191c5e131f1d">πŸ“§ 문의 Β· μ˜¨ν”„λ ˆλ―ΈμŠ€ Β· 제휴</a>
</span>
</div>
<div class="main">
<div class="left">
<div class="section">
<div class="label">πŸ“Œ ν”„λ‘¬ν”„νŠΈ</div>
<textarea id="prompt" rows="3" placeholder="예: 2026λ…„ AI λ³΄μ•ˆ μœ λ§κΈ°μ—… μœ‘μ„± 지원사업 곡λͺ¨ μ•ˆλ‚΄λ¬Έμ„ μž‘μ„±ν•΄μ£Όμ„Έμš”."></textarea>
</div>
<div class="section">
<div class="label">πŸ“Ž 레퍼런슀 λ¬Έμ„œ</div>
<div class="file-drop" id="fileDrop" onclick="document.getElementById('fileInput').click()">
<div class="file-icon">πŸ“„</div>
<div class="file-hint">클릭 λ˜λŠ” λ“œλž˜κ·Έν•˜μ—¬ μ—…λ‘œλ“œ</div>
<div class="file-hint" style="margin-top:2px;">HWP Β· HWPX Β· PDF Β· DOCX Β· TXT</div>
<div class="file-name" id="fileName"></div>
</div>
<input type="file" id="fileInput" accept=".hwp,.hwpx,.hml,.pdf,.docx,.txt,.md,.csv,.json,.xml,.xlsx">
</div>
<div class="section" id="modeSection">
<div class="label">βš™οΈ 생성 λͺ¨λ“œ</div>
<div class="mode-group" id="modeGroup">
<label class="mode-opt active" data-mode="1" onclick="selectMode(1)">
<input type="radio" name="genMode" value="1" checked>
<div class="mo-text">
<span class="mo-label">μƒˆλ‘œ 생성</span>
<span class="mo-desc">AIκ°€ μ£Όμ œμ— λ§žλŠ” λ¬Έμ„œλ₯Ό μ²˜μŒλΆ€ν„° μž‘μ„±</span>
</div>
</label>
<label class="mode-opt disabled" data-mode="2" onclick="selectMode(2)">
<input type="radio" name="genMode" value="2" disabled>
<div class="mo-text">
<span class="mo-label">μ„œμ‹ μœ μ§€ Β· λ‚΄μš© λ³€κ²½ <span class="mo-badge">πŸ“Ž λ¬Έμ„œ ν•„μš”</span></span>
<span class="mo-desc">원본 λ ˆμ΄μ•„μ›ƒ 100% 보쑴, ν…μŠ€νŠΈλ§Œ ꡐ체</span>
</div>
</label>
<label class="mode-opt disabled" data-mode="3" onclick="selectMode(3)">
<input type="radio" name="genMode" value="3" disabled>
<div class="mo-text">
<span class="mo-label">ꡬ쑰 μ°Έκ³  Β· μƒˆλ‘œ 생성 <span class="mo-badge">πŸ“Ž λ¬Έμ„œ ν•„μš”</span></span>
<span class="mo-desc">원본 ꡬ쑰λ₯Ό μ°Έκ³ ν•˜μ—¬ μƒˆ λ‚΄μš©μœΌλ‘œ μž‘μ„±</span>
</div>
</label>
</div>
</div>
<div class="section">
<div class="slider-row">
<div class="slider-group">
<div class="slider-label"><span>πŸ” 검색</span><span class="slider-val" id="valSearch">20</span></div>
<input type="range" min="5" max="100" step="5" value="20" id="slSearch" oninput="document.getElementById('valSearch').textContent=this.value">
</div>
<div class="slider-group">
<div class="slider-label"><span>🌑 Temp</span><span class="slider-val" id="valTemp">0.6</span></div>
<input type="range" min="0.1" max="1.0" step="0.05" value="0.6" id="slTemp" oninput="document.getElementById('valTemp').textContent=this.value">
</div>
</div>
</div>
<div class="section">
<div class="btn-row">
<button class="btn btn-primary" id="runBtn" onclick="runPipeline()">
<span class="spinner" id="btnSpin"></span>
<span id="btnLabel">πŸš€ λ¬Έμ„œ 생성</span>
</button>
<button class="btn btn-secondary" onclick="stopPipeline()" style="width:44px" title="쀑지">β›”</button>
</div>
<div class="status-bar"><span class="dot status-idle" id="statusDot"></span><span id="statusText">λŒ€κΈ° 쀑</span></div>
</div>
<div class="section">
<div class="dl-area">
<div class="label" style="margin-bottom:4px">πŸ“₯ HWPX λ³€ν™˜</div>
<div class="dl-fname" id="dlFname">파일 μ—†μŒ</div>
<div class="btn-row-2">
<button class="btn btn-primary btn-sm" id="dlBtn" onclick="downloadHwpx()" disabled>πŸ“₯ HWPX</button>
<button class="btn btn-secondary btn-sm" onclick="copyDoc()">πŸ“‹ 볡사</button>
</div>
<div class="dl-status" id="dlStatus"></div>
</div>
</div>
<!-- Doc Chat β€” 레퍼런슀 λ¬Έμ„œ 기반 QnA -->
<div class="acc open" id="accChat">
<div class="acc-header" onclick="toggleAcc('accChat')"><span class="acc-arrow">β–Ά</span> πŸ’¬ λ¬Έμ„œ QnA μ±—</div>
<div class="acc-body">
<div id="chatDocStatus" style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-bottom:6px;">πŸ“Ž 레퍼런슀 λ¬Έμ„œλ₯Ό μ—…λ‘œλ“œν•˜λ©΄ μžλ™ μ—°λ™λ©λ‹ˆλ‹€</div>
<div id="chatMessages" style="max-height:220px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--r-sm);padding:8px;margin-bottom:8px;background:var(--bg2);font-size:11px;line-height:1.7;"></div>
<div style="display:grid;grid-template-columns:1fr auto;gap:6px;">
<input type="text" id="chatInput" placeholder="λ¬Έμ„œμ— λŒ€ν•΄ μ§ˆλ¬Έν•˜μ„Έμš”..." style="font-size:12px;padding:8px 10px;" onkeydown="if(event.key==='Enter')sendChat()">
<button class="btn btn-primary btn-sm" onclick="sendChat()" style="width:40px;">πŸš€</button>
</div>
</div>
</div>
<div class="acc" id="accDoc">
<div class="acc-header" onclick="toggleAcc('accDoc')"><span class="acc-arrow">β–Ά</span> πŸ“ μƒμ„±λœ ν…μŠ€νŠΈ</div>
<div class="acc-body"><textarea id="docArea" rows="10" placeholder="λ¬Έμ„œ 생성 ν›„ ν‘œμ‹œλ©λ‹ˆλ‹€"></textarea></div>
</div>
<div class="acc" id="accStream">
<div class="acc-header" onclick="toggleAcc('accStream')"><span class="acc-arrow">β–Ά</span> ⚑ μ—μ΄μ „νŠΈ 슀트림</div>
<div class="acc-body"><textarea id="streamArea" rows="8" readonly placeholder="μ—μ΄μ „νŠΈ μ‹€μ‹œκ°„ 좜λ ₯"></textarea></div>
</div>
<div class="acc" id="accLog">
<div class="acc-header" onclick="toggleAcc('accLog')"><span class="acc-arrow">β–Ά</span> 🧬 νŒŒμ΄ν”„λΌμΈ 둜그</div>
<div class="acc-body"><textarea id="logArea" rows="6" readonly placeholder="νŒŒμ΄ν”„λΌμΈ 둜그"></textarea></div>
</div>
</div>
<div class="right">
<div class="vt">
<div class="vt-dots"><span style="background:#ff5f57"></span><span style="background:#febc2e"></span><span style="background:#28c840"></span></div>
<span class="vt-fname" id="viewerFname">πŸ“„ sample_hanji.hwpx</span>
<span class="vt-badge badge-e" id="badgeEngine">Python lxml</span>
<span class="vt-badge badge-r" id="badgeRender">HWPX Β· Full Render v3</span>
</div>
<div class="vb" id="viewerBody">
<div class="ve"><div class="ve-icon">⏳</div><div class="ve-text">λ‘œλ”© 쀑...</div></div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<a id="dlLink" style="display:none"></a>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script>
let running=false,finalDoc='',sc=0,currentMode=1,hasRefDoc=false,refIsHwpx=false;
function getBase(){return '';}
function showToast(m,t='info'){const e=document.getElementById('toast');e.className='toast '+t;e.textContent=m;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),3000);}
function toggleAcc(id){document.getElementById(id).classList.toggle('open');}
function setStatus(t,s='idle'){document.getElementById('statusText').textContent=t;document.getElementById('statusDot').className='dot status-'+s;}
function selectMode(m){
if(m>1 && !hasRefDoc)return;
if(m===2 && !refIsHwpx)return;
currentMode=m;
document.querySelectorAll('.mode-opt').forEach(function(el){
var v=+el.dataset.mode;
el.classList.toggle('active',v===m);
el.querySelector('input[type=radio]').checked=(v===m);
});
}
function updateModeAvailability(){
document.querySelectorAll('.mode-opt').forEach(function(el){
var v=+el.dataset.mode;
var badge=el.querySelector('.mo-badge');
if(v===1){
el.classList.remove('disabled');el.querySelector('input').disabled=false;
if(badge)badge.style.display='none';
} else if(v===2){
var ok=hasRefDoc&&refIsHwpx;
el.classList.toggle('disabled',!ok);el.querySelector('input').disabled=!ok;
if(badge)badge.style.display=ok?'none':'inline-block';
if(!ok&&currentMode===2){selectMode(1);}
} else if(v===3){
el.classList.toggle('disabled',!hasRefDoc);el.querySelector('input').disabled=!hasRefDoc;
if(badge)badge.style.display=hasRefDoc?'none':'inline-block';
if(!hasRefDoc&&currentMode===3){selectMode(1);}
}
});
}
const fi=document.getElementById('fileInput'),fd=document.getElementById('fileDrop'),fn=document.getElementById('fileName');
fi.addEventListener('change',function(){if(this.files.length)handleFile(this.files[0]);});
fd.addEventListener('dragover',e=>{e.preventDefault();fd.style.borderColor='var(--accent)';});
fd.addEventListener('dragleave',()=>{fd.style.borderColor='';});
fd.addEventListener('drop',e=>{e.preventDefault();fd.style.borderColor='';if(e.dataTransfer.files.length)handleFile(e.dataTransfer.files[0]);});
function handleFile(f){
fn.textContent='βœ… '+f.name+' ('+Math.round(f.size/1024)+'KB)';fn.style.display='block';fd.classList.add('has-file');
hasRefDoc=true;
const ext=f.name.split('.').pop().toLowerCase();
refIsHwpx=['hwp','hwpx'].includes(ext);
updateModeAvailability();
// μ±—μš© ν…μŠ€νŠΈ μΆ”μΆœ λ™μ‹œ μˆ˜ν–‰
uploadDocForChat(f);
// HWP/HWPX β†’ λ·°μ–΄ 미리보기
if(refIsHwpx){
const r=new FileReader();
r.onload=function(){renderB64(r.result.split(',')[1],'.'+ext,f.name);};
r.readAsDataURL(f);
}
}
function setEmpty(icon,msg){document.getElementById('viewerBody').innerHTML='<div class="ve"><div class="ve-icon">'+icon+'</div><div class="ve-text">'+msg+'</div></div>';}
async function renderPath(path,title){
setEmpty('⏳','λ Œλ”λ§ 쀑...');
document.getElementById('viewerFname').textContent='πŸ“„ '+title;
const b=document.getElementById('badgeRender');b.className='vt-badge badge-l';b.textContent='λ‘œλ”©...';
try{
const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:path})});
if(!r.ok)throw new Error(r.status);applyHTML(await r.text());
}catch(e){setEmpty('❌',e.message);b.className='vt-badge badge-e';b.textContent='Error';}
}
async function renderB64(b64,ext,title){
setEmpty('⏳','λ Œλ”λ§ 쀑...');
document.getElementById('viewerFname').textContent='πŸ“„ '+title;
const b=document.getElementById('badgeRender');b.className='vt-badge badge-l';b.textContent='λ‘œλ”©...';
try{
const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({b64,ext})});
if(!r.ok)throw new Error(r.status);applyHTML(await r.text());
}catch(e){setEmpty('❌',e.message);b.className='vt-badge badge-e';b.textContent='Error';}
}
function applyHTML(h){
const v=document.getElementById('viewerBody');
if(h.includes('srcdoc=')){v.innerHTML=h;}
else{v.innerHTML='<div style="position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;padding:16px;overflow:auto;">'+h+'</div>';}
const b=document.getElementById('badgeRender');
b.className='vt-badge badge-r';b.textContent=h.includes('ohah/hwpjs')?'✨ ohah/hwpjs WASM':'HWPX · Full Render v3';
}
async function runPipeline(){
const p=document.getElementById('prompt').value.trim();
if(!p){showToast('ν”„λ‘¬ν”„νŠΈλ₯Ό μž…λ ₯ν•˜μ„Έμš”.','err');return;}
running=true;finalDoc='';sc=0;window._transformFile=null;window._transformFname=null;window._transformPath=null;
document.getElementById('runBtn').classList.add('running');
document.getElementById('btnLabel').textContent='생성 쀑...';
document.getElementById('dlBtn').disabled=true;
setStatus('νŒŒμ΄ν”„λΌμΈ μ‹€ν–‰ 쀑...','run');
document.getElementById('accStream').classList.add('open');
try{
const r=await fetch(getBase()+'/soma/run',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({prompt:p,max_search:+document.getElementById('slSearch').value,temperature:+document.getElementById('slTemp').value,ref_sid:chatSid||'',mode:currentMode})});
if(!r.ok)throw new Error(await r.text());
const rd=r.body.getReader();const dec=new TextDecoder();let buf='',stream='',log='';
while(true){
const{done,value}=await rd.read();if(done||!running)break;
buf+=dec.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop();
for(const l of lines){
if(!l.startsWith('data: '))continue;const d=l.slice(6);
if(d==='[DONE]'){running=false;break;}
try{const c=JSON.parse(d);
if(c.stream){stream+=c.stream;document.getElementById('streamArea').value=stream.slice(-4000);}
if(c.log){document.getElementById('logArea').value=c.log;}
if(c.search_count!=null){sc=c.search_count;setStatus('πŸ” '+sc+'회 검색...','run');}
if(c.final_doc){finalDoc=c.final_doc;document.getElementById('docArea').value=finalDoc;}
if(c.done){finalDoc=c.final_doc||finalDoc;document.getElementById('docArea').value=finalDoc;
if(c.transform_file){window._transformFile=c.transform_file;window._transformFname=c.transform_filename||'λ³€ν™˜λ¬Έμ„œ.hwpx';window._transformPath=c.transform_path||null;}
}
}catch{}
}
}
document.getElementById('runBtn').classList.remove('running');
document.getElementById('btnLabel').textContent='πŸš€ λ¬Έμ„œ 생성';
document.getElementById('dlBtn').disabled=!finalDoc;
if(window._transformFile){
setStatus('βœ… λ¬Έμ„œ λ³€ν™˜ μ™„λ£Œ (μ„œμ‹ 100% 보쑴)','done');showToast('βœ… μ„œμ‹ 100% 보쑴 λ³€ν™˜ μ™„λ£Œ!','ok');
const a=document.getElementById('dlLink');a.href=getBase()+window._transformFile;a.download=window._transformFname;a.click();
document.getElementById('dlFname').textContent=window._transformFname;
document.getElementById('dlStatus').className='dl-status ok';document.getElementById('dlStatus').textContent='βœ“ λ³€ν™˜ λ‹€μš΄λ‘œλ“œ μ™„λ£Œ (μ„œμ‹ 100% 보쑴)';
document.getElementById('accDoc').classList.add('open');
try{if(window._transformPath)renderPath(window._transformPath,window._transformFname);else renderPath(null,window._transformFname);}catch(e){}
}else if(finalDoc){setStatus('βœ… μ™„λ£Œ Β· '+sc+'회 검색','done');showToast('βœ… λ¬Έμ„œ 생성 μ™„λ£Œ!','ok');document.getElementById('accDoc').classList.add('open');}
else setStatus('μ™„λ£Œ (λ¬Έμ„œ μ—†μŒ)','idle');
}catch(e){setStatus('❌ '+e.message,'err');showToast('❌ '+e.message,'err');
document.getElementById('runBtn').classList.remove('running');document.getElementById('btnLabel').textContent='πŸš€ λ¬Έμ„œ 생성';}
running=false;
}
function stopPipeline(){running=false;setStatus('β›” 쀑지됨','idle');document.getElementById('runBtn').classList.remove('running');document.getElementById('btnLabel').textContent='πŸš€ λ¬Έμ„œ 생성';showToast('β›” 쀑지됨');}
async function downloadHwpx(){
if(!finalDoc){showToast('λ¬Έμ„œλ₯Ό λ¨Όμ € μƒμ„±ν•˜μ„Έμš”.','err');return;}
document.getElementById('dlStatus').className='dl-status';document.getElementById('dlStatus').textContent='λ³€ν™˜ 쀑...';
try{
if(window._transformFile){
const a=document.getElementById('dlLink');a.href=getBase()+window._transformFile;a.download=window._transformFname||'λ³€ν™˜λ¬Έμ„œ.hwpx';a.click();
document.getElementById('dlFname').textContent=window._transformFname||'λ³€ν™˜λ¬Έμ„œ.hwpx';
document.getElementById('dlStatus').className='dl-status ok';document.getElementById('dlStatus').textContent='βœ“ λ³€ν™˜ λ‹€μš΄λ‘œλ“œ μ™„λ£Œ (μ„œμ‹ 100% 보쑴)';
showToast('βœ… HWPX λ³€ν™˜ λ‹€μš΄λ‘œλ“œ','ok');
window._transformFile=null;
return;
}
var maxRetry=2,lastErr='';
for(var attempt=0;attempt<=maxRetry;attempt++){
try{
if(attempt>0){document.getElementById('dlStatus').textContent='μž¬μ‹œλ„ '+(attempt)+'/'+maxRetry+'...';await new Promise(function(r){setTimeout(r,1500);});}
var r=await fetch(getBase()+'/soma/hml',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:finalDoc,ref_sid:chatSid||'',mode:currentMode})});
if(!r.ok){lastErr=r.status+' '+r.statusText;if(r.status>=500&&attempt<maxRetry)continue;throw new Error(await r.text());}
var d=await r.json();
if(d.file_url){
var a=document.getElementById('dlLink');a.href=getBase()+d.file_url;a.download=d.filename||'λ¬Έμ„œ.hwpx';a.click();
document.getElementById('dlFname').textContent=d.filename||'λ¬Έμ„œ.hwpx';
document.getElementById('dlStatus').className='dl-status ok';document.getElementById('dlStatus').textContent='βœ“ λ‹€μš΄λ‘œλ“œ μ™„λ£Œ'+(attempt>0?' (μž¬μ‹œλ„ 성곡)':'');
showToast('βœ… HWPX λ‹€μš΄λ‘œλ“œ','ok');
if(d.file_path)renderPath(d.file_path,d.filename||'λ¬Έμ„œ.hwpx');
return;
}else throw new Error('file_url μ—†μŒ');
}catch(retryErr){lastErr=retryErr.message;if(attempt>=maxRetry)throw retryErr;}
}
}catch(e){document.getElementById('dlStatus').className='dl-status err';document.getElementById('dlStatus').textContent='⚠ '+e.message;showToast('❌ '+e.message,'err');}
}
/* ── SIMPLE MD β†’ HTML ── */
function md2html(s){
return s
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/^### (.+)$/gm,'<div style="font-weight:700;font-size:12px;color:#1e40af;margin:8px 0 4px;border-left:3px solid #6366f1;padding-left:8px;">$1</div>')
.replace(/^## (.+)$/gm,'<div style="font-weight:800;font-size:13px;color:#1e3a5f;margin:10px 0 4px;padding-bottom:3px;border-bottom:1.5px solid #e2e8f0;">$1</div>')
.replace(/^# (.+)$/gm,'<div style="font-weight:900;font-size:14px;color:#0f172a;margin:12px 0 6px;">$1</div>')
.replace(/\*\*(.+?)\*\*/g,'<b>$1</b>')
.replace(/\*(.+?)\*/g,'<i>$1</i>')
.replace(/`([^`]+)`/g,'<code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;font-size:10px;color:#6366f1;">$1</code>')
.replace(/^- (.+)$/gm,'<div style="padding-left:14px;text-indent:-10px;margin:2px 0;">β€’ $1</div>')
.replace(/^\d+\. (.+)$/gm,function(m,p1){return '<div style="padding-left:14px;margin:2px 0;">'+m.match(/^\d+/)[0]+'. '+p1+'</div>';})
.replace(/\n{2,}/g,'<div style="margin:6px 0;"></div>')
.replace(/\n/g,'<br>');
}
function copyDoc(){const t=document.getElementById('docArea').value;if(!t){showToast('ν…μŠ€νŠΈ μ—†μŒ','err');return;}navigator.clipboard.writeText(t).then(()=>showToast('πŸ“‹ 볡사됨','ok'));}
/* ── DOC CHAT ── */
let chatSid='',chatHistory=[];
function addChatMsg(role,text){
const box=document.getElementById('chatMessages');
const color=role==='user'?'var(--accent)':role==='system'?'var(--muted)':'var(--teal)';
const label=role==='user'?'πŸ‘€':role==='system'?'ℹ️':'πŸ€–';
const rendered=role==='user'?text.replace(/\n/g,'<br>'):md2html(text);
box.innerHTML+='<div style="margin-bottom:6px;"><span style="font-weight:700;color:'+color+';font-size:10px;">'+label+'</span> <span>'+rendered+'</span></div>';
box.scrollTop=box.scrollHeight;
}
async function uploadDocForChat(file){
/* 레퍼런슀 μ—…λ‘œλ“œ μ‹œ μžλ™ 호좜 β€” μ±—μš© ν…μŠ€νŠΈ μΆ”μΆœ */
try{
const b64=await new Promise(function(res,rej){const r=new FileReader();r.onload=function(){res(r.result.split(',')[1]);};r.onerror=rej;r.readAsDataURL(file);});
const ext='.'+file.name.split('.').pop().toLowerCase();
const resp=await fetch(getBase()+'/soma/doc-upload',{method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({b64:b64,filename:file.name,ext:ext})});
const d=await resp.json();
if(d.ok){
chatSid=d.sid;chatHistory=[];
document.getElementById('chatDocStatus').innerHTML='βœ… <b>'+file.name+'</b> ('+d.chars.toLocaleString()+'자) 연동됨';
document.getElementById('chatMessages').innerHTML='';
addChatMsg('system','πŸ“„ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ ('+d.chars.toLocaleString()+'자). μ§ˆλ¬Έν•˜μ„Έμš”!');
}
}catch(e){
document.getElementById('chatDocStatus').textContent='❌ λ¬Έμ„œ 뢄석 μ‹€νŒ¨: '+e.message;
}
}
async function sendChat(){
var input=document.getElementById('chatInput');
var msg=input.value.trim();
if(!msg){return;}
input.value='';
addChatMsg('user',msg);
chatHistory.push([msg,'']);
try{
var r=await fetch(getBase()+'/soma/chat',{method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({message:msg,sid:chatSid,history:chatHistory.slice(-6)})});
if(!r.ok){throw new Error('μ„œλ²„ 였λ₯˜ '+r.status);}
var rd=r.body.getReader();
var dec=new TextDecoder();
var buf='',full='';
var box=document.getElementById('chatMessages');
var assistDiv=document.createElement('div');
assistDiv.style.marginBottom='6px';
assistDiv.innerHTML='<span style="font-weight:700;color:var(--teal);font-size:10px;">πŸ€–</span> <span id="chatStream"></span>';
box.appendChild(assistDiv);
var streamSpan=document.getElementById('chatStream');
while(true){
var result=await rd.read();
if(result.done){break;}
buf+=dec.decode(result.value,{stream:true});
var lines=buf.split('\n');
buf=lines.pop();
for(var i=0;i<lines.length;i++){
var l=lines[i];
if(l.indexOf('data: ')!==0){continue;}
var d=l.slice(6);
if(d==='[DONE]'){break;}
try{var c=JSON.parse(d);if(c.delta){full+=c.delta;streamSpan.innerHTML=md2html(full);}}catch(e2){}
}
box.scrollTop=box.scrollHeight;
}
// Remove temp id
streamSpan.removeAttribute('id');
chatHistory[chatHistory.length-1][1]=full;
}catch(e){addChatMsg('system','❌ '+e.message);}
}
window.addEventListener('DOMContentLoaded',async()=>{
try{const r=await fetch(getBase()+'/soma/status');const s=await r.json();document.getElementById('badgeEngine').textContent=s.engine||'Python lxml';}catch{}
try{
const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:'/app/sample_hanji.hwpx'})});
if(r.ok){applyHTML(await r.text());document.getElementById('viewerFname').textContent='πŸ“„ ν•œμ§€(HANJI) μ„œλΉ„μŠ€ μ†Œκ°œ';}
else setEmpty('πŸ“„','λ¬Έμ„œλ₯Ό μƒμ„±ν•˜κ±°λ‚˜ 레퍼런슀λ₯Ό μ—…λ‘œλ“œν•˜μ„Έμš”.');
}catch{setEmpty('πŸ“„','λ¬Έμ„œλ₯Ό μƒμ„±ν•˜κ±°λ‚˜ 레퍼런슀λ₯Ό μ—…λ‘œλ“œν•˜μ„Έμš”.');}
// μƒ˜ν”Œ λ¬Έμ„œ μ±— μžλ™ 연동
try{
var cr=await fetch(getBase()+'/soma/doc-upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:'/app/sample_hanji.hwpx'})});
var cd=await cr.json();
if(cd.ok){chatSid=cd.sid;chatHistory=[];
document.getElementById('chatDocStatus').innerHTML='βœ… μƒ˜ν”Œ λ¬Έμ„œ ('+cd.chars.toLocaleString()+'자) μžλ™ 연동';
document.getElementById('chatMessages').innerHTML='';
addChatMsg('system','πŸ“„ μƒ˜ν”Œ λ¬Έμ„œ λ‘œλ“œ μ™„λ£Œ. μ§ˆλ¬Έν•˜μ„Έμš”!');
}
}catch{}
});
</script>
</body>
</html>