Spaces:
Running
Running
| <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&¤tMode===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&¤tMode===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,'&').replace(/</g,'<') | |
| .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> |