CoolFace
Apppublic

ginigen/hwp-agent

sourceHugging Faceupdated 6mo agoView on Hugging Face
3likes
index.html571 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="ko">3<head>4<meta charset="UTF-8">5<meta name="viewport" content="width=device-width, initial-scale=1.0">6<title>한지 HANJI · On-Premise HWP AI Agent</title>7<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">8<style>9*{margin:0;padding:0;box-sizing:border-box;}10:root{11  --bg:#ffffff;--bg2:#f8f9fb;--bg3:#f1f3f6;12  --text:#1a1a2e;--text2:#475569;--muted:#94a3b8;13  --border:#e8ecf1;--border2:#d1d5db;14  --accent:#4f46e5;--accent2:#6366f1;--accent-light:rgba(99,102,241,.08);15  --teal:#0d9488;--teal-light:rgba(13,148,136,.08);16  --amber:#d97706;--red:#dc2626;--green:#16a34a;17  --r:12px;--r-sm:8px;18  --font:'Noto Sans KR',sans-serif;--mono:'JetBrains Mono',monospace;19  --shadow:0 4px 16px rgba(0,0,0,.06);--shadow-lg:0 8px 32px rgba(0,0,0,.08);20  --tr:.2s ease;21}22html,body{height:100%;overflow:hidden;font-family:var(--font);background:var(--bg);color:var(--text);font-size:13px;-webkit-font-smoothing:antialiased;}23::-webkit-scrollbar{width:5px;height:5px;}24::-webkit-scrollbar-thumb{background:var(--border2);border-radius:10px;}25::selection{background:var(--accent-light);color:var(--accent);}26@keyframes shimmer{0%,100%{background-position:0%}50%{background-position:100%}}27@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}28@keyframes spin{to{transform:rotate(360deg)}}29@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}30 31.topbar{height:46px;display:flex;align-items:center;gap:10px;padding:0 20px;background:var(--bg);border-bottom:1px solid var(--border);}32.logo{font-weight:900;font-size:15px;color:#1e1b4b;letter-spacing:-.3px;}33.logo em{font-style:normal;font-weight:400;font-size:11px;color:var(--text2);margin-left:2px;}34.topbar-sep{width:1px;height:18px;background:var(--border);margin:0 2px;}35.topbar-desc{font-size:11px;color:var(--text2);font-weight:500;}36.topbar-url{display:inline-flex;align-items:center;gap:4px;padding:3px 12px;border-radius:20px;37  font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:.3px;38  background:linear-gradient(135deg,#4f46e5,#4338ca);color:#fff;text-decoration:none;39  box-shadow:0 2px 8px rgba(79,70,229,.2);transition:var(--tr);}40.topbar-url:hover{box-shadow:0 4px 14px rgba(79,70,229,.35);transform:translateY(-1px);}41.topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;}42.topbar-contact{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;43  font-family:var(--mono);font-size:8.5px;font-weight:600;color:var(--teal);44  background:var(--teal-light);border:1px solid rgba(13,148,136,.12);45  text-decoration:none;transition:var(--tr);}46.topbar-contact:hover{background:rgba(13,148,136,.12);}47.chip{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;48  font-family:var(--mono);font-size:8px;font-weight:600;letter-spacing:.8px;49  background:var(--accent-light);color:var(--accent);border:1px solid rgba(99,102,241,.12);}50.chip-teal{background:var(--teal-light);color:var(--teal);border-color:rgba(13,148,136,.12);}51.dot-live{width:5px;height:5px;border-radius:50%;background:var(--green);animation:pulse 2s infinite;}52 53.main{display:grid;grid-template-columns:340px 1fr;height:calc(100vh - 50px);overflow:hidden;}54.left{border-right:1px solid var(--border);overflow-y:auto;padding:16px;background:var(--bg);}55.section{margin-bottom:14px;animation:fadeUp .4s ease both;}56.section:nth-child(2){animation-delay:.05s;}57.section:nth-child(3){animation-delay:.1s;}58.label{font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);margin-bottom:6px;}59textarea{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;}60textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-light);}61textarea::placeholder{color:var(--muted);font-size:11.5px;}62 63.file-drop{border:2px dashed var(--border);border-radius:var(--r);padding:18px;text-align:center;cursor:pointer;transition:var(--tr);background:var(--bg2);}64.file-drop:hover{border-color:var(--accent);background:var(--accent-light);}65.file-drop.has-file{border-color:var(--green);background:rgba(22,163,74,.04);}66.file-icon{font-size:22px;margin-bottom:4px;opacity:.4;}67.file-hint{font-size:10px;color:var(--muted);font-family:var(--mono);}68.file-name{font-size:11px;font-weight:600;color:var(--green);margin-top:4px;display:none;}69 70.slider-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;}71.slider-group{background:var(--bg2);border-radius:var(--r-sm);padding:8px 10px;}72.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;}73.slider-val{color:var(--accent);font-weight:700;}74input[type=range]{width:100%;height:4px;-webkit-appearance:none;background:var(--border);border-radius:4px;outline:none;}75input[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);}76 77.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%;}78.btn-primary{background:linear-gradient(135deg,var(--accent),#4338ca);color:#fff;box-shadow:0 4px 12px rgba(79,70,229,.25);}79.btn-primary:hover{box-shadow:0 6px 20px rgba(79,70,229,.35);transform:translateY(-1px);}80.btn-primary:active{transform:translateY(0);}81.btn-primary:disabled{opacity:.5;cursor:not-allowed;transform:none;}82.btn-secondary{background:var(--bg2);color:var(--text2);border:1px solid var(--border);}83.btn-secondary:hover{background:var(--bg3);}84.btn-sm{padding:6px 10px;font-size:10.5px;border-radius:6px;}85.btn-row{display:grid;grid-template-columns:1fr auto;gap:8px;}86.btn-row-2{display:grid;grid-template-columns:1fr 1fr;gap:8px;}87 88.status-bar{font-family:var(--mono);font-size:10px;color:var(--muted);padding:6px 0;display:flex;align-items:center;gap:6px;}89.dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;}90.status-idle{background:var(--muted);}91.status-run{background:var(--accent);animation:pulse 1.5s infinite;}92.status-done{background:var(--green);}93.status-err{background:var(--red);}94 95.acc{border:1px solid var(--border);border-radius:var(--r-sm);overflow:hidden;margin-bottom:8px;}96.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;}97.acc-header:hover{background:var(--bg3);}98.acc-arrow{font-size:8px;transition:var(--tr);color:var(--muted);}99.acc.open .acc-arrow{transform:rotate(90deg);}100.acc-body{display:none;padding:10px 12px;border-top:1px solid var(--border);background:var(--bg);}101.acc.open .acc-body{display:block;}102.acc textarea{font-size:11px;font-family:var(--mono);line-height:1.6;background:var(--bg2);border:none;}103 104.dl-area{background:var(--bg2);border-radius:var(--r-sm);padding:10px 12px;}105.dl-fname{font-family:var(--mono);font-size:10px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px;}106.dl-status{font-family:var(--mono);font-size:9px;margin-top:4px;}107.dl-status.ok{color:var(--green);}108.dl-status.err{color:var(--red);}109 110.right{background:#d8dce2;overflow:hidden;display:flex;flex-direction:column;}111.vt{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--bg);border-bottom:1px solid var(--border);flex-shrink:0;}112.vt-dots{display:flex;gap:5px;}113.vt-dots span{width:10px;height:10px;border-radius:50%;display:block;}114.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;}115.vt-badge{font-family:var(--mono);font-size:7.5px;font-weight:700;padding:2px 10px;border-radius:20px;letter-spacing:.5px;}116.badge-r{background:var(--teal-light);color:var(--teal);border:1px solid rgba(13,148,136,.15);}117.badge-e{background:var(--accent-light);color:var(--accent);border:1px solid rgba(99,102,241,.12);}118.badge-l{background:rgba(251,191,36,.1);color:var(--amber);border:1px solid rgba(251,191,36,.2);animation:pulse 1.5s infinite;}119 120.vb{flex:1;overflow:hidden;position:relative;}121.vb iframe{position:absolute;top:0;left:0;width:100%!important;height:100%!important;border:none!important;display:block;}122.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;}123.ve-icon{font-size:48px;opacity:.2;}124.ve-text{font-family:var(--mono);font-size:10px;text-align:center;line-height:1.8;}125 126.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);}127.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}128.toast.ok{background:var(--green);}.toast.err{background:var(--red);}.toast.info{background:var(--accent);}129 130.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;}131.running .spinner{display:inline-block;}132#fileInput{display:none;}133 134.mode-group{display:flex;flex-direction:column;gap:2px;}135.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;}136.mode-opt:hover{background:var(--bg2);}137.mode-opt.active{background:var(--accent-light);border-color:rgba(99,102,241,.15);}138.mode-opt.disabled{opacity:.35;cursor:not-allowed;pointer-events:none;}139.mode-opt input[type=radio]{margin-top:2px;accent-color:var(--accent);flex-shrink:0;}140.mode-opt .mo-text{display:flex;flex-direction:column;gap:1px;}141.mode-opt .mo-label{font-size:11.5px;font-weight:600;color:var(--text);}142.mode-opt .mo-desc{font-size:9.5px;color:var(--muted);line-height:1.4;}143.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;}144 145@media(max-width:900px){146  .main{grid-template-columns:1fr;grid-template-rows:auto 1fr;}147  .left{max-height:40vh;border-right:none;border-bottom:1px solid var(--border);}148}149</style>150</head>151<body>152 153<div class="topbar">154  <span class="logo">한지<em>(HANJI)</em></span>155  <span class="topbar-sep"></span>156  <span class="topbar-desc">HWP AI Agent 서비스</span>157  <a class="topbar-url" href="https://hanji.ginigen.ai" target="_blank">🔗 hanji.ginigen.ai</a>158  <span class="topbar-right">159    <a class="topbar-contact" href="/cdn-cgi/l/email-protection#7017191e1917151e1119180030171d11191c5e131f1d">📧 문의 · 온프레미스 · 제휴</a>160  </span>161</div>162 163<div class="main">164  <div class="left">165 166    <div class="section">167      <div class="label">📌 프롬프트</div>168      <textarea id="prompt" rows="3" placeholder="예: 2026년 AI 보안 유망기업 육성 지원사업 공모 안내문을 작성해주세요."></textarea>169    </div>170 171    <div class="section">172      <div class="label">📎 레퍼런스 문서</div>173      <div class="file-drop" id="fileDrop" onclick="document.getElementById('fileInput').click()">174        <div class="file-icon">📄</div>175        <div class="file-hint">클릭 또는 드래그하여 업로드</div>176        <div class="file-hint" style="margin-top:2px;">HWP · HWPX · PDF · DOCX · TXT</div>177        <div class="file-name" id="fileName"></div>178      </div>179      <input type="file" id="fileInput" accept=".hwp,.hwpx,.hml,.pdf,.docx,.txt,.md,.csv,.json,.xml,.xlsx">180    </div>181 182    <div class="section" id="modeSection">183      <div class="label">⚙️ 생성 모드</div>184      <div class="mode-group" id="modeGroup">185        <label class="mode-opt active" data-mode="1" onclick="selectMode(1)">186          <input type="radio" name="genMode" value="1" checked>187          <div class="mo-text">188            <span class="mo-label">새로 생성</span>189            <span class="mo-desc">AI가 주제에 맞는 문서를 처음부터 작성</span>190          </div>191        </label>192        <label class="mode-opt disabled" data-mode="2" onclick="selectMode(2)">193          <input type="radio" name="genMode" value="2" disabled>194          <div class="mo-text">195            <span class="mo-label">서식 유지 · 내용 변경 <span class="mo-badge">📎 문서 필요</span></span>196            <span class="mo-desc">원본 레이아웃 100% 보존, 텍스트만 교체</span>197          </div>198        </label>199        <label class="mode-opt disabled" data-mode="3" onclick="selectMode(3)">200          <input type="radio" name="genMode" value="3" disabled>201          <div class="mo-text">202            <span class="mo-label">구조 참고 · 새로 생성 <span class="mo-badge">📎 문서 필요</span></span>203            <span class="mo-desc">원본 구조를 참고하여 새 내용으로 작성</span>204          </div>205        </label>206      </div>207    </div>208 209    <div class="section">210      <div class="slider-row">211        <div class="slider-group">212          <div class="slider-label"><span>🔍 검색</span><span class="slider-val" id="valSearch">20</span></div>213          <input type="range" min="5" max="100" step="5" value="20" id="slSearch" oninput="document.getElementById('valSearch').textContent=this.value">214        </div>215        <div class="slider-group">216          <div class="slider-label"><span>🌡 Temp</span><span class="slider-val" id="valTemp">0.6</span></div>217          <input type="range" min="0.1" max="1.0" step="0.05" value="0.6" id="slTemp" oninput="document.getElementById('valTemp').textContent=this.value">218        </div>219      </div>220    </div>221 222    <div class="section">223      <div class="btn-row">224        <button class="btn btn-primary" id="runBtn" onclick="runPipeline()">225          <span class="spinner" id="btnSpin"></span>226          <span id="btnLabel">🚀 문서 생성</span>227        </button>228        <button class="btn btn-secondary" onclick="stopPipeline()" style="width:44px" title="중지">⛔</button>229      </div>230      <div class="status-bar"><span class="dot status-idle" id="statusDot"></span><span id="statusText">대기 중</span></div>231    </div>232 233    <div class="section">234      <div class="dl-area">235        <div class="label" style="margin-bottom:4px">📥 HWPX 변환</div>236        <div class="dl-fname" id="dlFname">파일 없음</div>237        <div class="btn-row-2">238          <button class="btn btn-primary btn-sm" id="dlBtn" onclick="downloadHwpx()" disabled>📥 HWPX</button>239          <button class="btn btn-secondary btn-sm" onclick="copyDoc()">📋 복사</button>240        </div>241        <div class="dl-status" id="dlStatus"></div>242      </div>243    </div>244 245    <!-- Doc Chat — 레퍼런스 문서 기반 QnA -->246    <div class="acc open" id="accChat">247      <div class="acc-header" onclick="toggleAcc('accChat')"><span class="acc-arrow">▶</span> 💬 문서 QnA 챗</div>248      <div class="acc-body">249        <div id="chatDocStatus" style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-bottom:6px;">📎 레퍼런스 문서를 업로드하면 자동 연동됩니다</div>250        <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>251        <div style="display:grid;grid-template-columns:1fr auto;gap:6px;">252          <input type="text" id="chatInput" placeholder="문서에 대해 질문하세요..." style="font-size:12px;padding:8px 10px;" onkeydown="if(event.key==='Enter')sendChat()">253          <button class="btn btn-primary btn-sm" onclick="sendChat()" style="width:40px;">🚀</button>254        </div>255      </div>256    </div>257 258    <div class="acc" id="accDoc">259      <div class="acc-header" onclick="toggleAcc('accDoc')"><span class="acc-arrow">▶</span> 📝 생성된 텍스트</div>260      <div class="acc-body"><textarea id="docArea" rows="10" placeholder="문서 생성 후 표시됩니다"></textarea></div>261    </div>262    <div class="acc" id="accStream">263      <div class="acc-header" onclick="toggleAcc('accStream')"><span class="acc-arrow">▶</span> ⚡ 에이전트 스트림</div>264      <div class="acc-body"><textarea id="streamArea" rows="8" readonly placeholder="에이전트 실시간 출력"></textarea></div>265    </div>266    <div class="acc" id="accLog">267      <div class="acc-header" onclick="toggleAcc('accLog')"><span class="acc-arrow">▶</span> 🧬 파이프라인 로그</div>268      <div class="acc-body"><textarea id="logArea" rows="6" readonly placeholder="파이프라인 로그"></textarea></div>269    </div>270  </div>271 272  <div class="right">273    <div class="vt">274      <div class="vt-dots"><span style="background:#ff5f57"></span><span style="background:#febc2e"></span><span style="background:#28c840"></span></div>275      <span class="vt-fname" id="viewerFname">📄 sample_hanji.hwpx</span>276      <span class="vt-badge badge-e" id="badgeEngine">Python lxml</span>277      <span class="vt-badge badge-r" id="badgeRender">HWPX · Full Render v3</span>278    </div>279    <div class="vb" id="viewerBody">280      <div class="ve"><div class="ve-icon">⏳</div><div class="ve-text">로딩 중...</div></div>281    </div>282  </div>283</div>284 285<div class="toast" id="toast"></div>286<a id="dlLink" style="display:none"></a>287 288<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script>289let running=false,finalDoc='',sc=0,currentMode=1,hasRefDoc=false,refIsHwpx=false;290function getBase(){return '';}291 292function 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);}293function toggleAcc(id){document.getElementById(id).classList.toggle('open');}294function setStatus(t,s='idle'){document.getElementById('statusText').textContent=t;document.getElementById('statusDot').className='dot status-'+s;}295 296function selectMode(m){297  if(m>1 && !hasRefDoc)return;298  if(m===2 && !refIsHwpx)return;299  currentMode=m;300  document.querySelectorAll('.mode-opt').forEach(function(el){301    var v=+el.dataset.mode;302    el.classList.toggle('active',v===m);303    el.querySelector('input[type=radio]').checked=(v===m);304  });305}306function updateModeAvailability(){307  document.querySelectorAll('.mode-opt').forEach(function(el){308    var v=+el.dataset.mode;309    var badge=el.querySelector('.mo-badge');310    if(v===1){311      el.classList.remove('disabled');el.querySelector('input').disabled=false;312      if(badge)badge.style.display='none';313    } else if(v===2){314      var ok=hasRefDoc&&refIsHwpx;315      el.classList.toggle('disabled',!ok);el.querySelector('input').disabled=!ok;316      if(badge)badge.style.display=ok?'none':'inline-block';317      if(!ok&&currentMode===2){selectMode(1);}318    } else if(v===3){319      el.classList.toggle('disabled',!hasRefDoc);el.querySelector('input').disabled=!hasRefDoc;320      if(badge)badge.style.display=hasRefDoc?'none':'inline-block';321      if(!hasRefDoc&&currentMode===3){selectMode(1);}322    }323  });324}325 326const fi=document.getElementById('fileInput'),fd=document.getElementById('fileDrop'),fn=document.getElementById('fileName');327fi.addEventListener('change',function(){if(this.files.length)handleFile(this.files[0]);});328fd.addEventListener('dragover',e=>{e.preventDefault();fd.style.borderColor='var(--accent)';});329fd.addEventListener('dragleave',()=>{fd.style.borderColor='';});330fd.addEventListener('drop',e=>{e.preventDefault();fd.style.borderColor='';if(e.dataTransfer.files.length)handleFile(e.dataTransfer.files[0]);});331 332function handleFile(f){333  fn.textContent='✅ '+f.name+' ('+Math.round(f.size/1024)+'KB)';fn.style.display='block';fd.classList.add('has-file');334  hasRefDoc=true;335  const ext=f.name.split('.').pop().toLowerCase();336  refIsHwpx=['hwp','hwpx'].includes(ext);337  updateModeAvailability();338  // 챗용 텍스트 추출 동시 수행339  uploadDocForChat(f);340  // HWP/HWPX → 뷰어 미리보기341  if(refIsHwpx){342    const r=new FileReader();343    r.onload=function(){renderB64(r.result.split(',')[1],'.'+ext,f.name);};344    r.readAsDataURL(f);345  }346}347 348function setEmpty(icon,msg){document.getElementById('viewerBody').innerHTML='<div class="ve"><div class="ve-icon">'+icon+'</div><div class="ve-text">'+msg+'</div></div>';}349 350async function renderPath(path,title){351  setEmpty('⏳','렌더링 중...');352  document.getElementById('viewerFname').textContent='📄 '+title;353  const b=document.getElementById('badgeRender');b.className='vt-badge badge-l';b.textContent='로딩...';354  try{355    const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:path})});356    if(!r.ok)throw new Error(r.status);applyHTML(await r.text());357  }catch(e){setEmpty('❌',e.message);b.className='vt-badge badge-e';b.textContent='Error';}358}359 360async function renderB64(b64,ext,title){361  setEmpty('⏳','렌더링 중...');362  document.getElementById('viewerFname').textContent='📄 '+title;363  const b=document.getElementById('badgeRender');b.className='vt-badge badge-l';b.textContent='로딩...';364  try{365    const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({b64,ext})});366    if(!r.ok)throw new Error(r.status);applyHTML(await r.text());367  }catch(e){setEmpty('❌',e.message);b.className='vt-badge badge-e';b.textContent='Error';}368}369 370function applyHTML(h){371  const v=document.getElementById('viewerBody');372  if(h.includes('srcdoc=')){v.innerHTML=h;}373  else{v.innerHTML='<div style="position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;padding:16px;overflow:auto;">'+h+'</div>';}374  const b=document.getElementById('badgeRender');375  b.className='vt-badge badge-r';b.textContent=h.includes('ohah/hwpjs')?'✨ ohah/hwpjs WASM':'HWPX · Full Render v3';376}377 378async function runPipeline(){379  const p=document.getElementById('prompt').value.trim();380  if(!p){showToast('프롬프트를 입력하세요.','err');return;}381  running=true;finalDoc='';sc=0;window._transformFile=null;window._transformFname=null;window._transformPath=null;382  document.getElementById('runBtn').classList.add('running');383  document.getElementById('btnLabel').textContent='생성 중...';384  document.getElementById('dlBtn').disabled=true;385  setStatus('파이프라인 실행 중...','run');386  document.getElementById('accStream').classList.add('open');387  try{388    const r=await fetch(getBase()+'/soma/run',{method:'POST',headers:{'Content-Type':'application/json'},389      body:JSON.stringify({prompt:p,max_search:+document.getElementById('slSearch').value,temperature:+document.getElementById('slTemp').value,ref_sid:chatSid||'',mode:currentMode})});390    if(!r.ok)throw new Error(await r.text());391    const rd=r.body.getReader();const dec=new TextDecoder();let buf='',stream='',log='';392    while(true){393      const{done,value}=await rd.read();if(done||!running)break;394      buf+=dec.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop();395      for(const l of lines){396        if(!l.startsWith('data: '))continue;const d=l.slice(6);397        if(d==='[DONE]'){running=false;break;}398        try{const c=JSON.parse(d);399          if(c.stream){stream+=c.stream;document.getElementById('streamArea').value=stream.slice(-4000);}400          if(c.log){document.getElementById('logArea').value=c.log;}401          if(c.search_count!=null){sc=c.search_count;setStatus('🔍 '+sc+'회 검색...','run');}402          if(c.final_doc){finalDoc=c.final_doc;document.getElementById('docArea').value=finalDoc;}403          if(c.done){finalDoc=c.final_doc||finalDoc;document.getElementById('docArea').value=finalDoc;404            if(c.transform_file){window._transformFile=c.transform_file;window._transformFname=c.transform_filename||'변환문서.hwpx';window._transformPath=c.transform_path||null;}405          }406        }catch{}407      }408    }409    document.getElementById('runBtn').classList.remove('running');410    document.getElementById('btnLabel').textContent='🚀 문서 생성';411    document.getElementById('dlBtn').disabled=!finalDoc;412    if(window._transformFile){413      setStatus('✅ 문서 변환 완료 (서식 100% 보존)','done');showToast('✅ 서식 100% 보존 변환 완료!','ok');414      const a=document.getElementById('dlLink');a.href=getBase()+window._transformFile;a.download=window._transformFname;a.click();415      document.getElementById('dlFname').textContent=window._transformFname;416      document.getElementById('dlStatus').className='dl-status ok';document.getElementById('dlStatus').textContent='✓ 변환 다운로드 완료 (서식 100% 보존)';417      document.getElementById('accDoc').classList.add('open');418      try{if(window._transformPath)renderPath(window._transformPath,window._transformFname);else renderPath(null,window._transformFname);}catch(e){}419    }else if(finalDoc){setStatus('✅ 완료 · '+sc+'회 검색','done');showToast('✅ 문서 생성 완료!','ok');document.getElementById('accDoc').classList.add('open');}420    else setStatus('완료 (문서 없음)','idle');421  }catch(e){setStatus('❌ '+e.message,'err');showToast('❌ '+e.message,'err');422    document.getElementById('runBtn').classList.remove('running');document.getElementById('btnLabel').textContent='🚀 문서 생성';}423  running=false;424}425function stopPipeline(){running=false;setStatus('⛔ 중지됨','idle');document.getElementById('runBtn').classList.remove('running');document.getElementById('btnLabel').textContent='🚀 문서 생성';showToast('⛔ 중지됨');}426 427async function downloadHwpx(){428  if(!finalDoc){showToast('문서를 먼저 생성하세요.','err');return;}429  document.getElementById('dlStatus').className='dl-status';document.getElementById('dlStatus').textContent='변환 중...';430  try{431    if(window._transformFile){432      const a=document.getElementById('dlLink');a.href=getBase()+window._transformFile;a.download=window._transformFname||'변환문서.hwpx';a.click();433      document.getElementById('dlFname').textContent=window._transformFname||'변환문서.hwpx';434      document.getElementById('dlStatus').className='dl-status ok';document.getElementById('dlStatus').textContent='✓ 변환 다운로드 완료 (서식 100% 보존)';435      showToast('✅ HWPX 변환 다운로드','ok');436      window._transformFile=null;437      return;438    }439    var maxRetry=2,lastErr='';440    for(var attempt=0;attempt<=maxRetry;attempt++){441      try{442        if(attempt>0){document.getElementById('dlStatus').textContent='재시도 '+(attempt)+'/'+maxRetry+'...';await new Promise(function(r){setTimeout(r,1500);});}443        var r=await fetch(getBase()+'/soma/hml',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:finalDoc,ref_sid:chatSid||'',mode:currentMode})});444        if(!r.ok){lastErr=r.status+' '+r.statusText;if(r.status>=500&&attempt<maxRetry)continue;throw new Error(await r.text());}445        var d=await r.json();446        if(d.file_url){447          var a=document.getElementById('dlLink');a.href=getBase()+d.file_url;a.download=d.filename||'문서.hwpx';a.click();448          document.getElementById('dlFname').textContent=d.filename||'문서.hwpx';449          document.getElementById('dlStatus').className='dl-status ok';document.getElementById('dlStatus').textContent='✓ 다운로드 완료'+(attempt>0?' (재시도 성공)':'');450          showToast('✅ HWPX 다운로드','ok');451          if(d.file_path)renderPath(d.file_path,d.filename||'문서.hwpx');452          return;453        }else throw new Error('file_url 없음');454      }catch(retryErr){lastErr=retryErr.message;if(attempt>=maxRetry)throw retryErr;}455    }456  }catch(e){document.getElementById('dlStatus').className='dl-status err';document.getElementById('dlStatus').textContent='⚠ '+e.message;showToast('❌ '+e.message,'err');}457}458/* ── SIMPLE MD → HTML ── */459function md2html(s){460  return s461    .replace(/&/g,'&amp;').replace(/</g,'&lt;')462    .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>')463    .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>')464    .replace(/^# (.+)$/gm,'<div style="font-weight:900;font-size:14px;color:#0f172a;margin:12px 0 6px;">$1</div>')465    .replace(/\*\*(.+?)\*\*/g,'<b>$1</b>')466    .replace(/\*(.+?)\*/g,'<i>$1</i>')467    .replace(/`([^`]+)`/g,'<code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;font-size:10px;color:#6366f1;">$1</code>')468    .replace(/^- (.+)$/gm,'<div style="padding-left:14px;text-indent:-10px;margin:2px 0;">• $1</div>')469    .replace(/^\d+\. (.+)$/gm,function(m,p1){return '<div style="padding-left:14px;margin:2px 0;">'+m.match(/^\d+/)[0]+'. '+p1+'</div>';})470    .replace(/\n{2,}/g,'<div style="margin:6px 0;"></div>')471    .replace(/\n/g,'<br>');472}473 474function copyDoc(){const t=document.getElementById('docArea').value;if(!t){showToast('텍스트 없음','err');return;}navigator.clipboard.writeText(t).then(()=>showToast('📋 복사됨','ok'));}475 476/* ── DOC CHAT ── */477let chatSid='',chatHistory=[];478 479function addChatMsg(role,text){480  const box=document.getElementById('chatMessages');481  const color=role==='user'?'var(--accent)':role==='system'?'var(--muted)':'var(--teal)';482  const label=role==='user'?'👤':role==='system'?'ℹ️':'🤖';483  const rendered=role==='user'?text.replace(/\n/g,'<br>'):md2html(text);484  box.innerHTML+='<div style="margin-bottom:6px;"><span style="font-weight:700;color:'+color+';font-size:10px;">'+label+'</span> <span>'+rendered+'</span></div>';485  box.scrollTop=box.scrollHeight;486}487 488async function uploadDocForChat(file){489  /* 레퍼런스 업로드 시 자동 호출 — 챗용 텍스트 추출 */490  try{491    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);});492    const ext='.'+file.name.split('.').pop().toLowerCase();493    const resp=await fetch(getBase()+'/soma/doc-upload',{method:'POST',494      headers:{'Content-Type':'application/json'},body:JSON.stringify({b64:b64,filename:file.name,ext:ext})});495    const d=await resp.json();496    if(d.ok){497      chatSid=d.sid;chatHistory=[];498      document.getElementById('chatDocStatus').innerHTML='✅ <b>'+file.name+'</b> ('+d.chars.toLocaleString()+'자) 연동됨';499      document.getElementById('chatMessages').innerHTML='';500      addChatMsg('system','📄 문서 로드 완료 ('+d.chars.toLocaleString()+'자). 질문하세요!');501    }502  }catch(e){503    document.getElementById('chatDocStatus').textContent='❌ 문서 분석 실패: '+e.message;504  }505}506 507async function sendChat(){508  var input=document.getElementById('chatInput');509  var msg=input.value.trim();510  if(!msg){return;}511  input.value='';512  addChatMsg('user',msg);513  chatHistory.push([msg,'']);514 515  try{516    var r=await fetch(getBase()+'/soma/chat',{method:'POST',517      headers:{'Content-Type':'application/json'},518      body:JSON.stringify({message:msg,sid:chatSid,history:chatHistory.slice(-6)})});519    if(!r.ok){throw new Error('서버 오류 '+r.status);}520    var rd=r.body.getReader();521    var dec=new TextDecoder();522    var buf='',full='';523    var box=document.getElementById('chatMessages');524    var assistDiv=document.createElement('div');525    assistDiv.style.marginBottom='6px';526    assistDiv.innerHTML='<span style="font-weight:700;color:var(--teal);font-size:10px;">🤖</span> <span id="chatStream"></span>';527    box.appendChild(assistDiv);528    var streamSpan=document.getElementById('chatStream');529 530    while(true){531      var result=await rd.read();532      if(result.done){break;}533      buf+=dec.decode(result.value,{stream:true});534      var lines=buf.split('\n');535      buf=lines.pop();536      for(var i=0;i<lines.length;i++){537        var l=lines[i];538        if(l.indexOf('data: ')!==0){continue;}539        var d=l.slice(6);540        if(d==='[DONE]'){break;}541        try{var c=JSON.parse(d);if(c.delta){full+=c.delta;streamSpan.innerHTML=md2html(full);}}catch(e2){}542      }543      box.scrollTop=box.scrollHeight;544    }545    // Remove temp id546    streamSpan.removeAttribute('id');547    chatHistory[chatHistory.length-1][1]=full;548  }catch(e){addChatMsg('system','❌ '+e.message);}549}550 551window.addEventListener('DOMContentLoaded',async()=>{552  try{const r=await fetch(getBase()+'/soma/status');const s=await r.json();document.getElementById('badgeEngine').textContent=s.engine||'Python lxml';}catch{}553  try{554    const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:'/app/sample_hanji.hwpx'})});555    if(r.ok){applyHTML(await r.text());document.getElementById('viewerFname').textContent='📄 한지(HANJI) 서비스 소개';}556    else setEmpty('📄','문서를 생성하거나 레퍼런스를 업로드하세요.');557  }catch{setEmpty('📄','문서를 생성하거나 레퍼런스를 업로드하세요.');}558  // 샘플 문서 챗 자동 연동559  try{560    var cr=await fetch(getBase()+'/soma/doc-upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:'/app/sample_hanji.hwpx'})});561    var cd=await cr.json();562    if(cd.ok){chatSid=cd.sid;chatHistory=[];563      document.getElementById('chatDocStatus').innerHTML='✅ 샘플 문서 ('+cd.chars.toLocaleString()+'자) 자동 연동';564      document.getElementById('chatMessages').innerHTML='';565      addChatMsg('system','📄 샘플 문서 로드 완료. 질문하세요!');566    }567  }catch{}568});569</script>570</body>571</html>