Spaces:
Running
Running
Upload 9 files
Browse files- .gitattributes +1 -0
- Dockerfile +21 -0
- README.md +8 -12
- app.py +634 -0
- core.cpython-312-x86_64-linux-gnu.so +3 -0
- emergence_matrix_document.json +356 -0
- engine.dat +1 -0
- index.html +471 -0
- requirements.txt +11 -0
- sample_hanji.hwpx +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
core.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 4 |
+
zip nodejs npm && \
|
| 5 |
+
rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY core.cpython-312-x86_64-linux-gnu.so ./
|
| 13 |
+
COPY engine.dat app.py index.html sample_hanji.hwpx ./
|
| 14 |
+
COPY emergence_matrix_document.json ./
|
| 15 |
+
COPY hwpx_templates/ ./hwpx_templates/
|
| 16 |
+
|
| 17 |
+
# 검증
|
| 18 |
+
RUN python -c "from core import soma_pipeline, generate_hwpx, hwpx_to_html_preview; print('✅ core.so 검증 통과')"
|
| 19 |
+
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,14 +1,10 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
pinned: false
|
| 10 |
-
license: apache-2.0
|
| 11 |
-
short_description: hwp-agent
|
| 12 |
---
|
| 13 |
-
|
| 14 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: HWP-Agent
|
| 3 |
+
emoji: 📄
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
+
# 한지(HANJI) · On-Premise HWP AI Agent
|
|
|
app.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================
|
| 2 |
+
# 한지(HANJI) · HWP AI Agent — Application Server
|
| 3 |
+
# core.py (또는 core.so) 에서 엔진 로드
|
| 4 |
+
# ============================================================
|
| 5 |
+
import os, re, json, tempfile, threading, zipfile
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
|
| 10 |
+
# ── 핵심 엔진 로드 (core.py 또는 core.so) ──
|
| 11 |
+
from core import (
|
| 12 |
+
soma_pipeline,
|
| 13 |
+
generate_hwpx,
|
| 14 |
+
hwpx_to_html_preview,
|
| 15 |
+
process_uploaded_file,
|
| 16 |
+
normalize_text_for_title,
|
| 17 |
+
ohaeng_cards_html,
|
| 18 |
+
analyze_hwpx_styles,
|
| 19 |
+
page_guard_check,
|
| 20 |
+
doc_chat_respond,
|
| 21 |
+
_viewer_empty,
|
| 22 |
+
_viewer_empty_fullpage,
|
| 23 |
+
_install_hwpjs,
|
| 24 |
+
_is_binary_hwp,
|
| 25 |
+
_HWPJS_READY,
|
| 26 |
+
_HWP_CONVERT_GUIDE,
|
| 27 |
+
FIREWORKS_API_KEY,
|
| 28 |
+
FIREWORKS_MODEL,
|
| 29 |
+
FIREWORKS_URL,
|
| 30 |
+
DOC_CHAT_SYSTEM,
|
| 31 |
+
)
|
| 32 |
+
import requests # chat API에서 사용
|
| 33 |
+
|
| 34 |
+
# ============================================================
|
| 35 |
+
# ⑦ Gradio UI — v7 HANJI · Single-Page HWP Viewer Style
|
| 36 |
+
# ============================================================
|
| 37 |
+
|
| 38 |
+
# ── 기본 샘플 문서 (서비스 시작 시 뷰어에 표시) ──
|
| 39 |
+
_SAMPLE_HWPX = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sample_hanji.hwpx")
|
| 40 |
+
_SAMPLE_PREVIEW = ""
|
| 41 |
+
if os.path.exists(_SAMPLE_HWPX):
|
| 42 |
+
try:
|
| 43 |
+
_SAMPLE_PREVIEW = hwpx_to_html_preview(_SAMPLE_HWPX)
|
| 44 |
+
print(f"✅ 기본 샘플 미리보기 로드: sample_hanji.hwpx ({os.path.getsize(_SAMPLE_HWPX):,}B)")
|
| 45 |
+
except Exception as _se:
|
| 46 |
+
print(f"⚠️ 샘플 미리보기 실패: {_se}")
|
| 47 |
+
if not _SAMPLE_PREVIEW:
|
| 48 |
+
_SAMPLE_PREVIEW = _viewer_empty_fullpage("문서를 생성하거나 레퍼런스를 업로드하면 여기에 표시됩니다.")
|
| 49 |
+
|
| 50 |
+
SOMA_CUSTOM_CSS = """
|
| 51 |
+
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}
|
| 52 |
+
.soma-topbar{display:flex;align-items:center;gap:10px;padding:8px 16px;
|
| 53 |
+
background:#fff;border:1px solid #e8ecf1;border-radius:12px;margin-bottom:8px;}
|
| 54 |
+
.soma-logo{font-weight:900;font-size:15px;color:#1e1b4b;}
|
| 55 |
+
.soma-logo em{font-style:normal;font-weight:400;font-size:11px;color:#475569;margin-left:2px;}
|
| 56 |
+
.soma-sep{width:1px;height:18px;background:#e8ecf1;}
|
| 57 |
+
.soma-desc{font-size:11px;color:#475569;font-weight:500;}
|
| 58 |
+
.soma-url{display:inline-flex;align-items:center;gap:4px;padding:3px 12px;border-radius:20px;
|
| 59 |
+
font-family:monospace;font-size:9.5px;font-weight:600;
|
| 60 |
+
background:linear-gradient(135deg,#4f46e5,#4338ca);color:#fff;text-decoration:none;
|
| 61 |
+
box-shadow:0 2px 8px rgba(79,70,229,.2);}
|
| 62 |
+
.soma-url:hover{box-shadow:0 4px 14px rgba(79,70,229,.35);}
|
| 63 |
+
.soma-right{margin-left:auto;display:flex;align-items:center;gap:8px;}
|
| 64 |
+
.soma-contact{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;
|
| 65 |
+
font-family:monospace;font-size:8.5px;font-weight:600;color:#0d9488;
|
| 66 |
+
background:rgba(13,148,136,.06);border:1px solid rgba(13,148,136,.12);text-decoration:none;}
|
| 67 |
+
button.primary{background:linear-gradient(135deg,#6366f1,#4f46e5)!important;border:none!important;
|
| 68 |
+
box-shadow:0 4px 12px rgba(99,102,241,.25)!important;font-weight:700!important;border-radius:10px!important;}
|
| 69 |
+
button.primary:hover{box-shadow:0 6px 20px rgba(99,102,241,.35)!important;}
|
| 70 |
+
.viewer-panel{min-height:700px;}
|
| 71 |
+
.viewer-panel .gr-html{min-height:680px;}
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
def build_ui():
|
| 75 |
+
with gr.Blocks(title="한지(HANJI) · HWP AI Agent 서비스") as app:
|
| 76 |
+
|
| 77 |
+
gr.HTML(f"<style>{SOMA_CUSTOM_CSS}</style>")
|
| 78 |
+
|
| 79 |
+
# ── Top Bar ──
|
| 80 |
+
gr.HTML("""
|
| 81 |
+
<div class="soma-topbar">
|
| 82 |
+
<span class="soma-logo">한지<em>(HANJI)</em></span>
|
| 83 |
+
<span class="soma-sep"></span>
|
| 84 |
+
<span class="soma-desc">HWP AI Agent 서비스</span>
|
| 85 |
+
<a class="soma-url" href="https://hanji.ginigen.ai" target="_blank">🔗 hanji.ginigen.ai</a>
|
| 86 |
+
<span class="soma-right">
|
| 87 |
+
<a class="soma-contact" href="mailto:ginigenaihp@gmail.com">📧 문의 · 온프레미스 · 제휴</a>
|
| 88 |
+
</span>
|
| 89 |
+
</div>""")
|
| 90 |
+
|
| 91 |
+
# ── States ──
|
| 92 |
+
ref_text_state = gr.State("")
|
| 93 |
+
ref_hwpx_path_state = gr.State("")
|
| 94 |
+
state = gr.State({"final_doc": "", "search_count": 0})
|
| 95 |
+
dummy_state = gr.State("")
|
| 96 |
+
doc_text_state = gr.State("")
|
| 97 |
+
|
| 98 |
+
# ══════════════════════════════════════════════════
|
| 99 |
+
# MAIN LAYOUT: Left 1/3 Controls | Right 2/3 Viewer
|
| 100 |
+
# ══════════════════════════════════════════════════
|
| 101 |
+
with gr.Row(equal_height=False):
|
| 102 |
+
|
| 103 |
+
# ── LEFT PANEL (1/3) ──────────────────────────
|
| 104 |
+
with gr.Column(scale=1, min_width=320):
|
| 105 |
+
|
| 106 |
+
# Prompt
|
| 107 |
+
prompt_input = gr.Textbox(
|
| 108 |
+
label="📌 프롬프트",
|
| 109 |
+
placeholder="예: 2026년 AI 보안 유망기업 육성 지원사업 공모 안내문을 작���해주세요.",
|
| 110 |
+
lines=3)
|
| 111 |
+
|
| 112 |
+
# File upload
|
| 113 |
+
ref_file_upload = gr.File(
|
| 114 |
+
label="📎 레퍼런스 문서",
|
| 115 |
+
file_types=[".hwp",".hwpx",".hml",".pdf",".docx",".txt",".md",
|
| 116 |
+
".csv",".json",".xml",".xlsx",".xls",".py",".html",".log"])
|
| 117 |
+
ref_upload_status = gr.Textbox(label="파일 상태", interactive=False, lines=2,
|
| 118 |
+
placeholder="레퍼런스 파일을 업로드하면 여기에 상태가 표시됩니다.")
|
| 119 |
+
|
| 120 |
+
# Settings (compact)
|
| 121 |
+
with gr.Row():
|
| 122 |
+
max_search_slider = gr.Slider(minimum=5, maximum=100, value=20, step=5,
|
| 123 |
+
label="🔍 검색", scale=1)
|
| 124 |
+
temperature_slider = gr.Slider(minimum=0.1, maximum=1.0, value=0.6, step=0.05,
|
| 125 |
+
label="🌡 Temp", scale=1)
|
| 126 |
+
|
| 127 |
+
# Action buttons
|
| 128 |
+
with gr.Row():
|
| 129 |
+
run_btn = gr.Button("🚀 문서 생성", variant="primary", scale=2)
|
| 130 |
+
stop_btn = gr.Button("⛔", variant="secondary", scale=0)
|
| 131 |
+
|
| 132 |
+
# Status indicator
|
| 133 |
+
search_counter = gr.Markdown("대기 중")
|
| 134 |
+
|
| 135 |
+
# HWPX Download
|
| 136 |
+
with gr.Row():
|
| 137 |
+
gen_hml_btn = gr.Button("📥 HWPX 변환", variant="primary", scale=2)
|
| 138 |
+
copy_text_btn = gr.Button("📋", variant="secondary", scale=0)
|
| 139 |
+
hml_status = gr.Textbox(label="", interactive=False, value="",
|
| 140 |
+
placeholder="HWPX 변환 상태", lines=1)
|
| 141 |
+
hml_file = gr.File(label="다운로드", file_types=[".hwpx"], visible=True)
|
| 142 |
+
|
| 143 |
+
# Generated text (collapsed)
|
| 144 |
+
with gr.Accordion("📝 생성된 텍스트", open=False):
|
| 145 |
+
final_doc_box = gr.Textbox(label="", value="", interactive=True, lines=12,
|
| 146 |
+
placeholder="SOMA 파이프라인 실행 후 최종 문서 텍스트")
|
| 147 |
+
|
| 148 |
+
# Pipeline internals (collapsed)
|
| 149 |
+
with gr.Accordion("🧬 파이프라인 로그", open=False):
|
| 150 |
+
agent_stream = gr.Textbox(label="Agent Stream", value="", interactive=False, lines=6)
|
| 151 |
+
search_log_box = gr.Textbox(label="Search Log", value="", interactive=False, lines=4)
|
| 152 |
+
agent_log_box = gr.Textbox(label="Pipeline Log", value="", interactive=False, lines=6)
|
| 153 |
+
|
| 154 |
+
# Doc Chat (collapsed)
|
| 155 |
+
with gr.Accordion("📎 문서 분석 챗", open=False):
|
| 156 |
+
doc_upload = gr.File(label="📄 문서 업로드",
|
| 157 |
+
file_types=[".hwp",".hwpx",".hml",".pdf",".docx",".txt",".md",
|
| 158 |
+
".csv",".json",".xml",".xlsx",".xls",".py",".html",".log"])
|
| 159 |
+
doc_upload_status = gr.Textbox(label="", interactive=False, lines=1)
|
| 160 |
+
doc_chatbot = gr.Chatbot(label="💬 Chat", height=200)
|
| 161 |
+
with gr.Row():
|
| 162 |
+
doc_msg = gr.Textbox(label="", placeholder="질문하세요...", lines=1, scale=4)
|
| 163 |
+
doc_send_btn = gr.Button("🚀", variant="primary", scale=0)
|
| 164 |
+
doc_clear_btn = gr.Button("🗑️ Clear", size="sm")
|
| 165 |
+
|
| 166 |
+
# ── RIGHT PANEL (2/3) — DOCUMENT VIEWER ──────
|
| 167 |
+
with gr.Column(scale=2, min_width=500, elem_classes=["viewer-panel"]):
|
| 168 |
+
viewer_main = gr.HTML(value=_SAMPLE_PREVIEW)
|
| 169 |
+
|
| 170 |
+
# ── Hidden component for ohaeng (pipeline needs it) ──
|
| 171 |
+
ohaeng_display = gr.HTML(value="", visible=False)
|
| 172 |
+
|
| 173 |
+
# ── Event Handlers
|
| 174 |
+
def handle_ref_upload(file):
|
| 175 |
+
if file is None:
|
| 176 |
+
return "", "", "", _viewer_empty("파일을 선택하면 여기에 미리보기가 표시됩니다.")
|
| 177 |
+
fpath = file.name if hasattr(file, 'name') else str(file)
|
| 178 |
+
fname = os.path.basename(fpath)
|
| 179 |
+
ext = Path(fpath).suffix.lower()
|
| 180 |
+
|
| 181 |
+
# ── 바이너리 HWP 감지 → 변환 안내 ──
|
| 182 |
+
if ext == '.hwp' and _is_binary_hwp(fpath):
|
| 183 |
+
text, err = process_uploaded_file(fpath)
|
| 184 |
+
preview = hwpx_to_html_preview(fpath)
|
| 185 |
+
if text:
|
| 186 |
+
status = f"📄 {fname} ({len(text):,}자 추출)\n\n{_HWP_CONVERT_GUIDE}"
|
| 187 |
+
return text, status, "", preview
|
| 188 |
+
return "", f"❌ {fname}: {err}", "", preview
|
| 189 |
+
|
| 190 |
+
# ── HWPX 파일 → 스타일 복원 모드 ──
|
| 191 |
+
hwpx_path = ""
|
| 192 |
+
if ext == '.hwpx':
|
| 193 |
+
try:
|
| 194 |
+
with zipfile.ZipFile(fpath, 'r') as zf:
|
| 195 |
+
if 'Contents/header.xml' in zf.namelist():
|
| 196 |
+
hwpx_path = fpath
|
| 197 |
+
styles = analyze_hwpx_styles(fpath)
|
| 198 |
+
print(f"📋 레퍼런스 HWPX 분석 완료: charPr {styles['char_count']}개, "
|
| 199 |
+
f"paraPr {styles['para_count']}개, "
|
| 200 |
+
f"borderFill {styles['bf_count']}개")
|
| 201 |
+
except:
|
| 202 |
+
pass
|
| 203 |
+
|
| 204 |
+
# ── 그 외 파일 ──
|
| 205 |
+
text, err = process_uploaded_file(fpath)
|
| 206 |
+
# 뷰어: HWP/HWPX만 렌더링
|
| 207 |
+
if ext in ('.hwp', '.hwpx'):
|
| 208 |
+
preview = hwpx_to_html_preview(fpath)
|
| 209 |
+
else:
|
| 210 |
+
preview = _viewer_empty(f"{fname} — HWP/HWPX 파일만 미리보기 지원됩니다.")
|
| 211 |
+
if text:
|
| 212 |
+
status = f"✅ {fname} ({len(text):,}자)"
|
| 213 |
+
if hwpx_path:
|
| 214 |
+
status += " 🎯 HWPX 스타일 복원 모드 활성"
|
| 215 |
+
status += "\n💡 원본 서식만 사용, 내용은 프롬프트 주제로 새로 생성됩니다"
|
| 216 |
+
return text, status, hwpx_path, preview
|
| 217 |
+
return "", f"❌ {fname}: {err}", "", preview
|
| 218 |
+
|
| 219 |
+
ref_file_upload.change(fn=handle_ref_upload, inputs=[ref_file_upload],
|
| 220 |
+
outputs=[ref_text_state, ref_upload_status, ref_hwpx_path_state, viewer_main])
|
| 221 |
+
|
| 222 |
+
def run_soma(prompt, max_search, temperature, ref_text, ref_hwpx_path=""):
|
| 223 |
+
if not prompt.strip():
|
| 224 |
+
yield (ohaeng_cards_html("水"), "⚠️ 프롬프트를 입력하세요.", "", "", "", "대기 중", "")
|
| 225 |
+
return
|
| 226 |
+
|
| 227 |
+
full_prompt = prompt
|
| 228 |
+
if ref_text and ref_text.strip():
|
| 229 |
+
# HWPX 레퍼런스가 있으면 → 서식 전용, AI 프롬프트에 원본 내용 미주입
|
| 230 |
+
# (SectionCloner가 서식을 처리하므로 내용은 프롬프트 주제만으로 생성)
|
| 231 |
+
if ref_hwpx_path and os.path.exists(ref_hwpx_path):
|
| 232 |
+
# HWPX 모드: 서식 구조 힌트만 간략히 추가
|
| 233 |
+
full_prompt = (
|
| 234 |
+
f"{prompt}\n\n"
|
| 235 |
+
f"[서식 참고: 공문서 형식으로 작성. "
|
| 236 |
+
f"# 제목, ## Ⅰ.~Ⅴ. 섹션, ### 소항목, □ 핵심항목, ○ 세부사항, "
|
| 237 |
+
f"| 표 | 형식을 활용하여 체계적으로 구성할 것]"
|
| 238 |
+
)
|
| 239 |
+
else:
|
| 240 |
+
# 일반 파일(PDF/TXT 등): 참고자료로 활용
|
| 241 |
+
ref_content = ref_text.strip()[:8000]
|
| 242 |
+
full_prompt = f"{prompt}\n\n[참고자료]\n{ref_content}"
|
| 243 |
+
|
| 244 |
+
stream_acc, log_acc, search_log, final_doc = "", "", "", ""
|
| 245 |
+
active_agent, sc = "水", 0
|
| 246 |
+
|
| 247 |
+
for chunk in soma_pipeline(full_prompt, int(max_search), temperature):
|
| 248 |
+
if chunk.get("done"):
|
| 249 |
+
final_doc = chunk.get("final_doc", "")
|
| 250 |
+
sc = chunk.get("search_count", sc)
|
| 251 |
+
log_acc = chunk.get("log", "")
|
| 252 |
+
search_log = chunk.get("search_log", "")
|
| 253 |
+
break
|
| 254 |
+
|
| 255 |
+
active_agent = chunk.get("active", active_agent)
|
| 256 |
+
tok = chunk.get("stream", "")
|
| 257 |
+
if tok:
|
| 258 |
+
stream_acc += tok
|
| 259 |
+
if len(stream_acc) > 8000:
|
| 260 |
+
stream_acc = "...(이전 생략)...\n" + stream_acc[-6000:]
|
| 261 |
+
|
| 262 |
+
if chunk.get("log"): log_acc = chunk["log"]
|
| 263 |
+
if chunk.get("search_log"): search_log = chunk["search_log"]
|
| 264 |
+
if chunk.get("search_count") is not None: sc = chunk["search_count"]
|
| 265 |
+
if chunk.get("final_doc"): final_doc = chunk["final_doc"]
|
| 266 |
+
|
| 267 |
+
yield (ohaeng_cards_html(active_agent), stream_acc, search_log, log_acc,
|
| 268 |
+
final_doc if final_doc else "", f"🔍 {sc} / {int(max_search)}", "")
|
| 269 |
+
|
| 270 |
+
yield (ohaeng_cards_html("金"), stream_acc + "\n\n🎉 완료!", search_log, log_acc,
|
| 271 |
+
final_doc, f"✅ 완료: {sc}회 검색", final_doc)
|
| 272 |
+
|
| 273 |
+
run_btn.click(
|
| 274 |
+
fn=run_soma,
|
| 275 |
+
inputs=[prompt_input, max_search_slider, temperature_slider, ref_text_state, ref_hwpx_path_state],
|
| 276 |
+
outputs=[ohaeng_display, agent_stream, search_log_box, agent_log_box, final_doc_box, search_counter, dummy_state])
|
| 277 |
+
|
| 278 |
+
def make_hml(doc_text, ref_hwpx_path):
|
| 279 |
+
if not doc_text or not doc_text.strip():
|
| 280 |
+
return None, "❌ 문서를 먼저 생성하세요.", _viewer_empty("HWPX 생성 후 여기에 표시됩니다.")
|
| 281 |
+
try:
|
| 282 |
+
# 레퍼런스 HWPX가 있으면 스타일 복원 모드
|
| 283 |
+
if ref_hwpx_path and os.path.exists(ref_hwpx_path):
|
| 284 |
+
path = generate_hwpx(doc_text.strip(), ref_hwpx_path=ref_hwpx_path)
|
| 285 |
+
mode = "🎯 레퍼런스 스타일 복원"
|
| 286 |
+
else:
|
| 287 |
+
path = generate_hwpx(doc_text.strip())
|
| 288 |
+
mode = "📄 report 템플릿"
|
| 289 |
+
|
| 290 |
+
title = normalize_text_for_title(doc_text.strip())
|
| 291 |
+
safe_title = re.sub(r'[\\/:*?"<>|]', '', title)[:40].strip() or "문서"
|
| 292 |
+
new_path = os.path.join(os.path.dirname(path), f"{safe_title}.hwpx")
|
| 293 |
+
os.rename(path, new_path)
|
| 294 |
+
|
| 295 |
+
# page_guard 결과 표시
|
| 296 |
+
status = f"✅ 생성 완료 ({mode})"
|
| 297 |
+
if ref_hwpx_path and os.path.exists(ref_hwpx_path):
|
| 298 |
+
guard = page_guard_check(ref_hwpx_path, new_path)
|
| 299 |
+
if guard["status"] == "PASS":
|
| 300 |
+
status += f" | 📏 page_guard PASS (ref={guard['ref_chars']}자 → out={guard['out_chars']}자)"
|
| 301 |
+
else:
|
| 302 |
+
status += f" | ⚠️ page_guard {len(guard['errors'])}건 경고"
|
| 303 |
+
|
| 304 |
+
# 생성된 HWPX 뷰어 렌더링
|
| 305 |
+
preview = hwpx_to_html_preview(new_path)
|
| 306 |
+
return new_path, status, preview
|
| 307 |
+
|
| 308 |
+
except Exception as e:
|
| 309 |
+
return None, f"❌ 오류: {e}", _viewer_empty(f"생성 오류: {e}")
|
| 310 |
+
|
| 311 |
+
gen_hml_btn.click(fn=make_hml, inputs=[final_doc_box, ref_hwpx_path_state],
|
| 312 |
+
outputs=[hml_file, hml_status, viewer_main])
|
| 313 |
+
|
| 314 |
+
# Doc Chat handlers
|
| 315 |
+
def handle_doc_upload(file):
|
| 316 |
+
if file is None:
|
| 317 |
+
return "", "파일을 선택해주세요."
|
| 318 |
+
fpath = file.name if hasattr(file, 'name') else str(file)
|
| 319 |
+
fname = os.path.basename(fpath)
|
| 320 |
+
ext = Path(fpath).suffix.lower()
|
| 321 |
+
|
| 322 |
+
# 바이너리 HWP 감지
|
| 323 |
+
is_bin_hwp = (ext == '.hwp' and _is_binary_hwp(fpath))
|
| 324 |
+
|
| 325 |
+
text, err = process_uploaded_file(fpath)
|
| 326 |
+
if text:
|
| 327 |
+
status = f"✅ {fname} ({len(text):,}자)"
|
| 328 |
+
if is_bin_hwp:
|
| 329 |
+
status = f"📄 {fname} ({len(text):,}자 추출) — 바이너리 HWP (텍스트만 추출됨)"
|
| 330 |
+
return text, status
|
| 331 |
+
return "", f"❌ {fname}: {err}"
|
| 332 |
+
|
| 333 |
+
doc_upload.change(fn=handle_doc_upload, inputs=[doc_upload], outputs=[doc_text_state, doc_upload_status])
|
| 334 |
+
doc_send_btn.click(fn=doc_chat_respond, inputs=[doc_msg, doc_chatbot, doc_text_state], outputs=[doc_chatbot])
|
| 335 |
+
doc_msg.submit(fn=doc_chat_respond, inputs=[doc_msg, doc_chatbot, doc_text_state], outputs=[doc_chatbot])
|
| 336 |
+
doc_clear_btn.click(fn=lambda: ([], ""), outputs=[doc_chatbot, doc_text_state])
|
| 337 |
+
|
| 338 |
+
return app
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
# ============================================================
|
| 342 |
+
# ⑧ Entry Point — FastAPI 메인 + Gradio 서브마운트
|
| 343 |
+
# ============================================================
|
| 344 |
+
from fastapi import FastAPI, Request as _FAReq
|
| 345 |
+
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse, StreamingResponse
|
| 346 |
+
import uvicorn
|
| 347 |
+
|
| 348 |
+
# ── FastAPI 메인 앱 ──
|
| 349 |
+
app = FastAPI()
|
| 350 |
+
|
| 351 |
+
_APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 352 |
+
_index_path = os.path.join(_APP_DIR, "index.html")
|
| 353 |
+
|
| 354 |
+
# ── ohah/hwpjs 백그라운드 설치 ──
|
| 355 |
+
threading.Thread(target=_install_hwpjs, daemon=True).start()
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# ── "/" → index.html 서빙 ──
|
| 359 |
+
@app.get("/")
|
| 360 |
+
async def _serve_index():
|
| 361 |
+
if os.path.exists(_index_path):
|
| 362 |
+
return FileResponse(_index_path, media_type="text/html")
|
| 363 |
+
return HTMLResponse("<h1>index.html not found</h1>", status_code=404)
|
| 364 |
+
|
| 365 |
+
@app.get("/ui")
|
| 366 |
+
async def _serve_ui():
|
| 367 |
+
return await _serve_index()
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# ── SOMA API ──
|
| 371 |
+
import asyncio as _asyncio
|
| 372 |
+
import queue as _queue
|
| 373 |
+
|
| 374 |
+
_file_registry = {}
|
| 375 |
+
_doc_text_store = {} # sid → text
|
| 376 |
+
|
| 377 |
+
@app.post("/soma/run")
|
| 378 |
+
async def _soma_run(req: _FAReq):
|
| 379 |
+
try:
|
| 380 |
+
body = await req.json()
|
| 381 |
+
prompt = body.get("prompt", "").strip()
|
| 382 |
+
max_search = int(body.get("max_search", 20))
|
| 383 |
+
temperature = float(body.get("temperature", 0.6))
|
| 384 |
+
ref_text = body.get("ref_text", "") # 직접 전달
|
| 385 |
+
ref_sid = body.get("ref_sid", "") # doc-upload에서 받은 sid
|
| 386 |
+
if not ref_text and ref_sid:
|
| 387 |
+
ref_text = _doc_text_store.get(ref_sid, "")
|
| 388 |
+
if not prompt:
|
| 389 |
+
return JSONResponse({"error": "prompt 없음"}, status_code=400)
|
| 390 |
+
|
| 391 |
+
# 레퍼런스 텍스트가 있으면 프롬프트에 추가
|
| 392 |
+
full_prompt = prompt
|
| 393 |
+
if ref_text and ref_text.strip():
|
| 394 |
+
ref_snippet = ref_text.strip()[:8000]
|
| 395 |
+
full_prompt = f"{prompt}\n\n[참고자료]\n{ref_snippet}"
|
| 396 |
+
|
| 397 |
+
# 동기 제너레이터를 별도 스레드에서 실행 → 이벤트 루프 블로킹 방지
|
| 398 |
+
q = _queue.Queue()
|
| 399 |
+
|
| 400 |
+
def _run_in_thread():
|
| 401 |
+
try:
|
| 402 |
+
for chunk in soma_pipeline(full_prompt, max_search, temperature):
|
| 403 |
+
q.put(json.dumps(chunk, ensure_ascii=False))
|
| 404 |
+
except Exception as e:
|
| 405 |
+
q.put(json.dumps({"error": str(e), "done": True}))
|
| 406 |
+
finally:
|
| 407 |
+
q.put(None) # 종료 시그널
|
| 408 |
+
|
| 409 |
+
threading.Thread(target=_run_in_thread, daemon=True).start()
|
| 410 |
+
|
| 411 |
+
async def _async_generate():
|
| 412 |
+
while True:
|
| 413 |
+
# 큐에서 비동기로 가져오기 (이벤트 루프 블로킹 없음)
|
| 414 |
+
try:
|
| 415 |
+
item = await _asyncio.get_event_loop().run_in_executor(
|
| 416 |
+
None, lambda: q.get(timeout=300))
|
| 417 |
+
except:
|
| 418 |
+
break
|
| 419 |
+
if item is None:
|
| 420 |
+
yield "data: [DONE]\n\n"
|
| 421 |
+
break
|
| 422 |
+
yield f"data: {item}\n\n"
|
| 423 |
+
|
| 424 |
+
return StreamingResponse(_async_generate(),
|
| 425 |
+
media_type="text/event-stream",
|
| 426 |
+
headers={"Cache-Control": "no-cache",
|
| 427 |
+
"X-Accel-Buffering": "no"})
|
| 428 |
+
except Exception as e:
|
| 429 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
@app.post("/soma/hml")
|
| 434 |
+
async def _soma_hml(req: _FAReq):
|
| 435 |
+
try:
|
| 436 |
+
body = await req.json()
|
| 437 |
+
content = body.get("content", "").strip()
|
| 438 |
+
if not content:
|
| 439 |
+
return JSONResponse({"error": "content 없음"}, status_code=400)
|
| 440 |
+
|
| 441 |
+
def _blocking():
|
| 442 |
+
path = generate_hwpx(content)
|
| 443 |
+
title = normalize_text_for_title(content)
|
| 444 |
+
safe = re.sub(r'[\\/:*?"<>|]', '', title)[:40].strip() or "문서"
|
| 445 |
+
new_path = os.path.join(os.path.dirname(path), f"{safe}.hwpx")
|
| 446 |
+
os.rename(path, new_path)
|
| 447 |
+
return new_path
|
| 448 |
+
|
| 449 |
+
new_path = await _asyncio.get_event_loop().run_in_executor(None, _blocking)
|
| 450 |
+
fname = os.path.basename(new_path)
|
| 451 |
+
_file_registry[fname] = new_path
|
| 452 |
+
|
| 453 |
+
return JSONResponse({"file_url": f"/file/{fname}",
|
| 454 |
+
"filename": fname,
|
| 455 |
+
"file_path": new_path})
|
| 456 |
+
except Exception as e:
|
| 457 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
@app.get("/file/{fname}")
|
| 461 |
+
async def _serve_file(fname: str):
|
| 462 |
+
fpath = _file_registry.get(fname)
|
| 463 |
+
if fpath and os.path.exists(fpath):
|
| 464 |
+
return FileResponse(fpath, filename=fname,
|
| 465 |
+
media_type="application/octet-stream")
|
| 466 |
+
return JSONResponse({"error": "파일 없음"}, status_code=404)
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
@app.post("/soma/preview")
|
| 470 |
+
async def _soma_preview(req: _FAReq):
|
| 471 |
+
try:
|
| 472 |
+
body = await req.json()
|
| 473 |
+
if "b64" in body:
|
| 474 |
+
import base64 as _b64
|
| 475 |
+
ext = body.get("ext", ".hwpx").lower()
|
| 476 |
+
tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
|
| 477 |
+
tmp.write(_b64.b64decode(body["b64"]))
|
| 478 |
+
tmp.close()
|
| 479 |
+
fpath = tmp.name
|
| 480 |
+
else:
|
| 481 |
+
fpath = body.get("file_path", "")
|
| 482 |
+
|
| 483 |
+
if not fpath or not os.path.exists(fpath):
|
| 484 |
+
return HTMLResponse(_viewer_empty("파일을 찾을 수 없습니다."))
|
| 485 |
+
|
| 486 |
+
preview = await _asyncio.get_event_loop().run_in_executor(
|
| 487 |
+
None, hwpx_to_html_preview, fpath)
|
| 488 |
+
return HTMLResponse(preview)
|
| 489 |
+
except Exception as e:
|
| 490 |
+
return HTMLResponse(_viewer_empty(f"미리보기 오류: {e}"))
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
@app.get("/soma/status")
|
| 494 |
+
async def _soma_status():
|
| 495 |
+
return JSONResponse({
|
| 496 |
+
"status": "ok",
|
| 497 |
+
"hwpjs_ready": _HWPJS_READY,
|
| 498 |
+
"engine": "ohah/hwpjs WASM" if _HWPJS_READY else "Python lxml"
|
| 499 |
+
})
|
| 500 |
+
|
| 501 |
+
# HF Spaces 호환 — 헬스체크
|
| 502 |
+
@app.get("/api/health")
|
| 503 |
+
async def _health():
|
| 504 |
+
return JSONResponse({"status": "ok"})
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
# ── 문서 업로드 (텍스트 추출) ──
|
| 508 |
+
|
| 509 |
+
@app.post("/soma/doc-upload")
|
| 510 |
+
async def _soma_doc_upload(req: _FAReq):
|
| 511 |
+
"""업로드된 문서에서 텍스트 추출 (b64 또는 file_path)"""
|
| 512 |
+
try:
|
| 513 |
+
body = await req.json()
|
| 514 |
+
|
| 515 |
+
fpath = body.get("file_path", "")
|
| 516 |
+
if not (fpath and os.path.exists(fpath)):
|
| 517 |
+
import base64 as _b64
|
| 518 |
+
b64 = body.get("b64", "")
|
| 519 |
+
fname = body.get("filename", "document")
|
| 520 |
+
ext = body.get("ext", ".txt").lower()
|
| 521 |
+
tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
|
| 522 |
+
tmp.write(_b64.b64decode(b64))
|
| 523 |
+
tmp.close()
|
| 524 |
+
fpath = tmp.name
|
| 525 |
+
|
| 526 |
+
text, err = await _asyncio.get_event_loop().run_in_executor(
|
| 527 |
+
None, process_uploaded_file, fpath)
|
| 528 |
+
|
| 529 |
+
if text:
|
| 530 |
+
sid = str(id(text))[-8:]
|
| 531 |
+
_doc_text_store[sid] = text
|
| 532 |
+
return JSONResponse({"ok": True, "sid": sid,
|
| 533 |
+
"chars": len(text),
|
| 534 |
+
"preview": text[:200]})
|
| 535 |
+
return JSONResponse({"ok": False, "error": err or "텍스트 추출 실패"})
|
| 536 |
+
except Exception as e:
|
| 537 |
+
return JSONResponse({"ok": False, "error": str(e)})
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
# ── 문서 QnA 챗 (SSE 스트리밍) ──
|
| 541 |
+
@app.post("/soma/chat")
|
| 542 |
+
async def _soma_chat(req: _FAReq):
|
| 543 |
+
"""문서 기반 QnA 챗 — SSE 스트리밍"""
|
| 544 |
+
try:
|
| 545 |
+
body = await req.json()
|
| 546 |
+
message = body.get("message", "").strip()
|
| 547 |
+
sid = body.get("sid", "")
|
| 548 |
+
history = body.get("history", [])
|
| 549 |
+
|
| 550 |
+
if not message:
|
| 551 |
+
return JSONResponse({"error": "message 없음"}, status_code=400)
|
| 552 |
+
if not FIREWORKS_API_KEY:
|
| 553 |
+
return JSONResponse({"error": "FIREWORKS_API_KEY 미설정"}, status_code=500)
|
| 554 |
+
|
| 555 |
+
doc_text = _doc_text_store.get(sid, "")
|
| 556 |
+
|
| 557 |
+
# 메시지 구성
|
| 558 |
+
if doc_text:
|
| 559 |
+
user_content = f"## 📄 업로드된 문서 내용\n---\n{doc_text[:12000]}\n---\n\n## 💬 질문\n{message}\n\n위 문서 내용을 바탕으로 답변해주세요."
|
| 560 |
+
else:
|
| 561 |
+
user_content = message
|
| 562 |
+
|
| 563 |
+
api_messages = [{"role": "system", "content": DOC_CHAT_SYSTEM}]
|
| 564 |
+
for h in (history or [])[-6:]:
|
| 565 |
+
if isinstance(h, (list, tuple, dict)):
|
| 566 |
+
if isinstance(h, dict):
|
| 567 |
+
api_messages.append({"role": h.get("role","user"), "content": h.get("content","")})
|
| 568 |
+
elif len(h) == 2:
|
| 569 |
+
api_messages.append({"role": "user", "content": h[0] or ""})
|
| 570 |
+
api_messages.append({"role": "assistant", "content": h[1] or ""})
|
| 571 |
+
api_messages.append({"role": "user", "content": user_content})
|
| 572 |
+
|
| 573 |
+
q2 = _queue.Queue()
|
| 574 |
+
|
| 575 |
+
def _chat_thread():
|
| 576 |
+
try:
|
| 577 |
+
headers = {"Accept":"application/json","Content-Type":"application/json",
|
| 578 |
+
"Authorization": f"Bearer {FIREWORKS_API_KEY}"}
|
| 579 |
+
payload = {"model": FIREWORKS_MODEL, "max_tokens": 16000,
|
| 580 |
+
"temperature": 0.6, "stream": True, "messages": api_messages}
|
| 581 |
+
resp = requests.post(FIREWORKS_URL, headers=headers, json=payload,
|
| 582 |
+
stream=True, timeout=180)
|
| 583 |
+
resp.raise_for_status()
|
| 584 |
+
for raw_line in resp.iter_lines():
|
| 585 |
+
if not raw_line: continue
|
| 586 |
+
line = raw_line.decode("utf-8") if isinstance(raw_line, bytes) else raw_line
|
| 587 |
+
if not line.startswith("data: "): continue
|
| 588 |
+
data = line[6:]
|
| 589 |
+
if data.strip() == "[DONE]":
|
| 590 |
+
break
|
| 591 |
+
try:
|
| 592 |
+
chunk = json.loads(data)
|
| 593 |
+
delta = chunk["choices"][0]["delta"].get("content", "")
|
| 594 |
+
if delta:
|
| 595 |
+
q2.put(json.dumps({"delta": delta}, ensure_ascii=False))
|
| 596 |
+
except:
|
| 597 |
+
pass
|
| 598 |
+
except Exception as e:
|
| 599 |
+
q2.put(json.dumps({"error": str(e)}))
|
| 600 |
+
finally:
|
| 601 |
+
q2.put(None)
|
| 602 |
+
|
| 603 |
+
threading.Thread(target=_chat_thread, daemon=True).start()
|
| 604 |
+
|
| 605 |
+
async def _async_chat():
|
| 606 |
+
while True:
|
| 607 |
+
try:
|
| 608 |
+
item = await _asyncio.get_event_loop().run_in_executor(
|
| 609 |
+
None, lambda: q2.get(timeout=300))
|
| 610 |
+
except:
|
| 611 |
+
break
|
| 612 |
+
if item is None:
|
| 613 |
+
yield "data: [DONE]\n\n"
|
| 614 |
+
break
|
| 615 |
+
yield f"data: {item}\n\n"
|
| 616 |
+
|
| 617 |
+
return StreamingResponse(_async_chat(),
|
| 618 |
+
media_type="text/event-stream",
|
| 619 |
+
headers={"Cache-Control": "no-cache",
|
| 620 |
+
"X-Accel-Buffering": "no"})
|
| 621 |
+
except Exception as e:
|
| 622 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
# ── Gradio를 /gradio 서브경로에 마운트 ──
|
| 626 |
+
demo = build_ui()
|
| 627 |
+
app = gr.mount_gradio_app(app, demo, path="/gradio")
|
| 628 |
+
|
| 629 |
+
print("✅ FastAPI 메인 서버")
|
| 630 |
+
print(" / → index.html")
|
| 631 |
+
print(" /gradio → Gradio UI")
|
| 632 |
+
print(" /soma/* → API")
|
| 633 |
+
|
| 634 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
core.cpython-312-x86_64-linux-gnu.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:480e7f95f1c788c03126ca8da468833ea268e6d6de00f5ab44ec9efbfef771f7
|
| 3 |
+
size 1650968
|
emergence_matrix_document.json
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_meta": {
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "6-Layer 문서작성 창발 매트릭스 — Koestler Bisociation 기반 이종 레이어 교차로 창발적 통찰 유도",
|
| 5 |
+
"philosophy": "이종 레이어 간 거리가 멀수록 높은 창발성 보너스. 에이전트가 자기 레이어 외 원격 레이어의 사고 패턴을 교차 적용할 때 독창적 문서가 탄생한다.",
|
| 6 |
+
"system": "SOMA Pre-AGI Document Emergence Engine",
|
| 7 |
+
"author": "Ginigen AI (지니젠AI)"
|
| 8 |
+
},
|
| 9 |
+
"layers": {
|
| 10 |
+
"RESEARCH": {
|
| 11 |
+
"desc": "탐색·수집 — 외부 세계의 정보를 탐지하고 정제하는 인지의 입구",
|
| 12 |
+
"agent": "水",
|
| 13 |
+
"layer_index": 0,
|
| 14 |
+
"strategies": {
|
| 15 |
+
"탐색_패턴": [
|
| 16 |
+
"수렴적 검색: 핵심 키워드를 좁혀가며 정밀 탐색",
|
| 17 |
+
"발산적 검색: 연관 키워드를 확장하며 의외의 연결고리 발견",
|
| 18 |
+
"시계열 추적: 동일 주제의 과거→현재→미래 변화 흐름 조사",
|
| 19 |
+
"반증 검색: 주장을 반박하는 자료를 의도적으로 탐색",
|
| 20 |
+
"교차 도메인 탐색: 전혀 다른 분야에서 유사 패턴 발견",
|
| 21 |
+
"원전 소급: 2차 자료에서 1차 원전(법령, 논문, 통계)으로 거슬러 올라감",
|
| 22 |
+
"이해관계자 매핑: 주제에 영향 받는 모든 주체별 관점 수집",
|
| 23 |
+
"메타 검색: '이 주제에 대한 기존 보고서/연구'를 검색하여 선행 프레임 파악"
|
| 24 |
+
],
|
| 25 |
+
"정보_품질_필터": [
|
| 26 |
+
"출처 위계: 공식 법령 > 정부 보고서 > 학술 논문 > 언론 > 블로그",
|
| 27 |
+
"시점 검증: 최신 데이터인지, 폐기된 정보인지 연도 확인",
|
| 28 |
+
"교차 검증: 동일 팩트를 3개 이상 독립 출처에서 확인",
|
| 29 |
+
"통계 맥락화: 절대값뿐 아니라 비율, 추세, 비교 대상까지 수집",
|
| 30 |
+
"인용 체인: 누가 누구를 인용하는지 추적하여 원천 신뢰도 평가"
|
| 31 |
+
],
|
| 32 |
+
"발견_프레임": [
|
| 33 |
+
"빈칸 탐지: 기존 자료에서 다루지 않는 영역(gap) 식별",
|
| 34 |
+
"이상값 포착: 예상과 다른 수치/사례를 발견하면 원인 추적",
|
| 35 |
+
"패턴 인식: 서로 다른 자료에서 반복되는 공통 패턴 추출",
|
| 36 |
+
"암묵지 발굴: 공식 문서에는 없지만 현장 인터뷰/사례에서 드러나는 실질 정보"
|
| 37 |
+
]
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"STRUCTURE": {
|
| 41 |
+
"desc": "설계·조직 — 정보를 체계적 골격으로 변환하는 건축 행위",
|
| 42 |
+
"agent": "木",
|
| 43 |
+
"layer_index": 1,
|
| 44 |
+
"strategies": {
|
| 45 |
+
"구조_아키타입": [
|
| 46 |
+
"두괄식(역피라미드): 결론 → 근거 → 배경. 의사결정자 대상 보고서",
|
| 47 |
+
"미괄식(서사형): 배경 → 분석 → 결론. 설득·제안 문서",
|
| 48 |
+
"병렬형: 동등한 주제를 나란히 배치. 비교·대안 분석",
|
| 49 |
+
"계층형(드릴다운): 거시 → 미시. 정책 → 세부 실행계획",
|
| 50 |
+
"문제-해결형: 현황(문제) → 원인 분석 → 해결 방안 → 기대효과",
|
| 51 |
+
"시계열형: 과거 → 현재 → 미래. 로드맵·추진 일정",
|
| 52 |
+
"MECE 분해: 상호배제·전체포괄. 빠짐없는 분류 체계"
|
| 53 |
+
],
|
| 54 |
+
"섹션_설계_원리": [
|
| 55 |
+
"1섹션 1메시지: 각 섹션이 전달하는 핵심 메시지 1개를 먼저 정의",
|
| 56 |
+
"전환 브릿지: 섹션 간 논리적 연결문으로 자연스러운 흐름 보장",
|
| 57 |
+
"깊이 균형: 중요 섹션은 깊게, 부수 섹션은 간결하게",
|
| 58 |
+
"표·그림 배치: 복잡한 데이터는 표로, 흐름은 도식으로 시각화",
|
| 59 |
+
"반복 삼각: 핵심 메시지를 서론·본론·결론에서 세 번 반복"
|
| 60 |
+
],
|
| 61 |
+
"창발적_구조": [
|
| 62 |
+
"역설 구조: 통념과 반대되는 결론을 먼저 제시하고 논증 전개",
|
| 63 |
+
"렌즈 전환: 동일 주제를 경제/기술/사회/법률 렌즈로 순환 분석",
|
| 64 |
+
"케이스 대비: 이론 섹션마다 실제 사례를 쌍으로 배치",
|
| 65 |
+
"미래 역산: 목표 상태를 먼저 정의하고 현재로 역산하여 단계 설계"
|
| 66 |
+
]
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"ARGUMENT": {
|
| 70 |
+
"desc": "논증·검증 — 주장을 단련하고 오류를 소각하는 비판적 사고 엔진",
|
| 71 |
+
"agent": "火",
|
| 72 |
+
"layer_index": 2,
|
| 73 |
+
"strategies": {
|
| 74 |
+
"논증_패턴": [
|
| 75 |
+
"연역법: 일반 원칙 → 구체 결론. 법령·정책 근거 활용",
|
| 76 |
+
"귀납법: 사례 축적 → 일반화. 데이터·통계 기반",
|
| 77 |
+
"유추법: A 분야 성공사례 → B 분야 적용 가능성 논증",
|
| 78 |
+
"소거법: 가능한 대안을 하나씩 배제하여 최적안 도출",
|
| 79 |
+
"비용편익: 정량적 비교로 실행 타당성 논증",
|
| 80 |
+
"반사실 논증: '만약 이 정책이 없었다면' 가상 시나리오로 필요성 입증"
|
| 81 |
+
],
|
| 82 |
+
"팩트체크_프로토콜": [
|
| 83 |
+
"수치 검증: 출처의 원본 데이터와 대조",
|
| 84 |
+
"논리 검증: 전제 → 결론 사이 비약이 없는지 점검",
|
| 85 |
+
"시점 검증: 데이터의 기준 시점이 문서 맥락에 적합한지 확인",
|
| 86 |
+
"범위 검증: 일부 사례를 전체로 일반화하는 오류 탐지",
|
| 87 |
+
"이해충돌 검증: 출처 기관의 이해관계가 데이터에 편향을 줄 가능성 점검"
|
| 88 |
+
],
|
| 89 |
+
"비판적_메타인지": [
|
| 90 |
+
"자기반박: 내가 작성한 논증에 대해 가장 강력한 반론을 스스로 제기",
|
| 91 |
+
"맹점 탐색: 내 분석에서 빠진 관점이 무엇인지 체계적으로 점검",
|
| 92 |
+
"확증편향 차단: 찬성 근거만 모았는지, 반대 근거도 균형 있게 수집했는지 자문",
|
| 93 |
+
"불확실성 명시: 확실한 것과 추정인 것을 구분하여 표현"
|
| 94 |
+
]
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"EVIDENCE": {
|
| 98 |
+
"desc": "통합·실체화 — 흩어진 조각을 하나의 완성된 문서로 주조하는 용광로",
|
| 99 |
+
"agent": "土",
|
| 100 |
+
"layer_index": 3,
|
| 101 |
+
"strategies": {
|
| 102 |
+
"통합_기법": [
|
| 103 |
+
"모자이크 통합: 각 에이전트 산출물의 최선 부분을 선별·재조합",
|
| 104 |
+
"논리 직조: 섹션 간 인과관계와 논리 체인을 명시적으로 연결",
|
| 105 |
+
"톤 통일: 전체 문서의 문체·경어법·전문용어 수준을 일관되게 조율",
|
| 106 |
+
"분량 밸런싱: 섹션별 분량을 중요도에 비례하여 재조정",
|
| 107 |
+
"중복 제거: 여러 에이전트가 반복한 내용을 단일화",
|
| 108 |
+
"표·데이터 삽입: 논증을 뒷받침하는 정량 데이터를 표 형식으로 정리"
|
| 109 |
+
],
|
| 110 |
+
"완성도_체크": [
|
| 111 |
+
"LATCH 구조 검증: Location·Alphabet·Time·Category·Hierarchy 중 적절한 분류 사용 여부",
|
| 112 |
+
"So-What 테스트: 각 문단의 마지막에 '그래서 어떻다는 것인가?'에 답이 있는지",
|
| 113 |
+
"Elevator Test: 문서 전체를 30초 요약으로 축약 가능한지",
|
| 114 |
+
"실행 가능성: 제안된 방안이 구체적 실행 단계를 포함하는지"
|
| 115 |
+
],
|
| 116 |
+
"독자_최적화": [
|
| 117 |
+
"독자 프로파일링: 의사결정권자/실무자/전문가 등 1차 독자 명확화",
|
| 118 |
+
"전문용어 게이트: 독자 수준에 맞는 용어 선택, 필요시 괄호 설명 추가",
|
| 119 |
+
"행동 유도: 문서를 읽은 후 독자가 취해야 할 구체적 행동 명시"
|
| 120 |
+
]
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"NARRATIVE": {
|
| 124 |
+
"desc": "서사·맥락 — 팩트를 이야기로 변환하여 기억과 설득력을 증폭하는 레이어",
|
| 125 |
+
"agent": null,
|
| 126 |
+
"layer_index": 4,
|
| 127 |
+
"strategies": {
|
| 128 |
+
"서사_기법": [
|
| 129 |
+
"프레이밍: 동일 사실을 '위기' 또는 '기회'로 틀 짓기에 따라 설득력 변화",
|
| 130 |
+
"구체화: 추상적 통계를 개인·기업·지역의 구체적 스토리로 변환",
|
| 131 |
+
"대비 효과: 변경 전/후, 국내/해외, 현재/미래를 나란히 보여주는 극적 대비",
|
| 132 |
+
"진행형 서사: '현재 진행 중인 변화'를 강조하여 시급성 부여",
|
| 133 |
+
"영웅의 여정: 문제 발견 → 시도와 실패 → 해결책 발견 → 성과 구조"
|
| 134 |
+
],
|
| 135 |
+
"수사_장치": [
|
| 136 |
+
"질문 삽입: '과연 이 정책은 효과가 있을까?' 같은 독자 참여형 질문",
|
| 137 |
+
"숫자의 힘: '연간 2.3조원'보다 '국민 1인당 월 3,800원' 같은 체감 환산",
|
| 138 |
+
"유추: 복잡한 시스템을 일상적 비유로 설명",
|
| 139 |
+
"점진적 공개: 결론을 즉시 노출하지 않고 단서를 쌓아가며 논리적 긴장감 조성"
|
| 140 |
+
],
|
| 141 |
+
"맥락_설정": [
|
| 142 |
+
"역사적 맥락: 이 정책/기술이 필요하게 된 역사적 배경",
|
| 143 |
+
"글로벌 맥락: 해외 주요국의 동향과 한국의 위치",
|
| 144 |
+
"기술적 맥락: 기반 기술의 성숙도와 실현 가능성",
|
| 145 |
+
"제도적 맥락: 관련 법령·규정·거버넌스 체계"
|
| 146 |
+
]
|
| 147 |
+
}
|
| 148 |
+
},
|
| 149 |
+
"IMPACT": {
|
| 150 |
+
"desc": "영향·가치 — 문서가 현실에 미치는 파급력과 의사결정 촉진력을 극대화",
|
| 151 |
+
"agent": "金",
|
| 152 |
+
"layer_index": 5,
|
| 153 |
+
"strategies": {
|
| 154 |
+
"영향력_증폭": [
|
| 155 |
+
"정량 기대효과: 비용 절감, 매출 증대, 효율 향상 등 수치로 표현",
|
| 156 |
+
"정성 기대효과: 국민 편익, 산업 경쟁력, 사회적 가치 등 질적 효과",
|
| 157 |
+
"리스크 완화: 미시행 시 발생할 부정적 영향을 대비 제시",
|
| 158 |
+
"단계별 마일스톤: 단기(3개월)/중기(1년)/장기(3년) 성과 로드맵",
|
| 159 |
+
"파급 효과 체인: 1차 효과 → 2차 파생 효과 → 산업 생태계 변화"
|
| 160 |
+
],
|
| 161 |
+
"최종_정제": [
|
| 162 |
+
"문체 일관성: 전체 문서의 어조·경어법·번호 체계 통일",
|
| 163 |
+
"용어 정밀도: 동의어 반복 제거, 약어 첫 등장시 풀네임 병기",
|
| 164 |
+
"논리 완결성: 서론의 문제제기가 결론에서 해결되는지 확인",
|
| 165 |
+
"형식 점검: 제목·번호·표·참고문헌 등 서식 규칙 준수",
|
| 166 |
+
"가독성 최적화: 문장 길이, 단락 크기, 여백 균형"
|
| 167 |
+
],
|
| 168 |
+
"메타인지_자기검증": [
|
| 169 |
+
"오류 복구: 이전 단계 에이전트의 실수를 감지하고 자체 수정",
|
| 170 |
+
"자기 반성: 내 정제가 과도하지 않은지, 원래 의도를 훼손하지 않았는지 점검",
|
| 171 |
+
"확신도 태깅: 각 주장에 대한 확신도(높음/중간/낮음)를 내부적으로 평가",
|
| 172 |
+
"개선 루프: 수정한 부분이 새로운 문제를 야기하지 않는지 재검토"
|
| 173 |
+
]
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
},
|
| 177 |
+
"cross_layer_bonus": {
|
| 178 |
+
"_desc": "레이어 간 거리가 멀수록 교차 적용 시 창발성이 높다. 값 = 창발성 보너스 가중치.",
|
| 179 |
+
"RESEARCH↔IMPACT": {
|
| 180 |
+
"bonus": 0.15,
|
| 181 |
+
"hint": "데이터에서 바로 시사점을 도출하면 가장 독창적 통찰이 탄생한다. 수집 단계에서 이미 '이 데이터가 의사결정에 어떤 영향을 줄까?'를 자문하라."
|
| 182 |
+
},
|
| 183 |
+
"RESEARCH↔NARRATIVE": {
|
| 184 |
+
"bonus": 0.12,
|
| 185 |
+
"hint": "건조한 데이터를 수집하면서 동시에 '이것이 어떤 이야기가 될 수 있을까?'를 상상하라."
|
| 186 |
+
},
|
| 187 |
+
"STRUCTURE↔IMPACT": {
|
| 188 |
+
"bonus": 0.11,
|
| 189 |
+
"hint": "구조 설계 단계에서 이미 '각 섹션이 최종 의사결정에 어떻게 기여하는가?'를 고려하라."
|
| 190 |
+
},
|
| 191 |
+
"STRUCTURE↔EVIDENCE": {
|
| 192 |
+
"bonus": 0.10,
|
| 193 |
+
"hint": "골격과 살을 동시에 고려하라. 구조가 근거를 자연스럽게 배치할 수 있는 형태인지 검증."
|
| 194 |
+
},
|
| 195 |
+
"ARGUMENT↔NARRATIVE": {
|
| 196 |
+
"bonus": 0.09,
|
| 197 |
+
"hint": "논증과 서사를 교차하라. 차가운 논리에 따뜻한 이야기를 입히면 설득력이 배가된다."
|
| 198 |
+
},
|
| 199 |
+
"RESEARCH↔ARGUMENT": {
|
| 200 |
+
"bonus": 0.06,
|
| 201 |
+
"hint": "수집과 검증을 동시에 수행하라. 자료를 모으면서 신뢰성을 실시간 평가."
|
| 202 |
+
},
|
| 203 |
+
"EVIDENCE↔IMPACT": {
|
| 204 |
+
"bonus": 0.05,
|
| 205 |
+
"hint": "통합 작성 시 최종 임팩트를 염두에 두면 불필요한 내용이 자연히 걸러진다."
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
"writing_principles": [
|
| 209 |
+
{
|
| 210 |
+
"id": 1,
|
| 211 |
+
"name": "모듈화(분할)",
|
| 212 |
+
"hint": "복잡한 주제를 독립적 하위 모듈로 분리하여 각각을 정밀하게 다루되, 최종 통합 시 시너지를 노려라.",
|
| 213 |
+
"application": "대주제를 3~5개 독립 섹션으로 분해. 각 섹션이 단독으로도 의미를 갖되 전체 조합 시 더 큰 그림을 형성."
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
"id": 2,
|
| 217 |
+
"name": "반론 선제(역발상)",
|
| 218 |
+
"hint": "독자가 제기할 반론을 예측하여 먼저 제시하고 논파하라. '그럼에도 불구하고' 구조.",
|
| 219 |
+
"application": "각 제안의 예상 비판(비용, 부작용, 실현가능성)을 미리 명시하고 대응 논리를 배치."
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
"id": 3,
|
| 223 |
+
"name": "관점 전환(차원변경)",
|
| 224 |
+
"hint": "동일 주제를 미시↔거시, 단기↔장기, 국내↔글로벌, 공급↔수요 등 다른 차원에서 바라보라.",
|
| 225 |
+
"application": "하나의 정책을 정부/기업/국민/국제사회 4개 관점에서 각각 분석하여 입체적 보고서 작성."
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"id": 4,
|
| 229 |
+
"name": "융합 제안(통합)",
|
| 230 |
+
"hint": "서로 관련 없어 보이는 분야를 연결하여 독창적 해법을 제시하라.",
|
| 231 |
+
"application": "A 분야의 해결법을 B 분야에 이식. 예: 핀테크 규제 경험을 헬스테크 규제에 적용."
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"id": 5,
|
| 235 |
+
"name": "위기→기회(전화위복)",
|
| 236 |
+
"hint": "문제점·리스크를 분석할 때, 그 자체를 차별화 기회로 전환할 수 없는지 탐색하라.",
|
| 237 |
+
"application": "규제 장벽 → '규제 준수 선점을 통한 경쟁 우위 확보' 같은 역전 논리."
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
"id": 6,
|
| 241 |
+
"name": "자기 참조(메타인지)",
|
| 242 |
+
"hint": "문서 자체가 자신의 한계와 전제를 투명하게 밝히면 신뢰도가 올라간다.",
|
| 243 |
+
"application": "분석의 한계, 데이터 제약, 추정 범위를 명시하여 독자가 판단할 근거 제공."
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"id": 7,
|
| 247 |
+
"name": "계단식 공개(점진적 복잡성)",
|
| 248 |
+
"hint": "간단한 개념에서 시작하여 점진적으로 복잡한 내용으로 진행하라.",
|
| 249 |
+
"application": "개요(1문단) → 핵심(2문단) → 세부(5문단) → 기술적 상세(부록) 구조."
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"id": 8,
|
| 253 |
+
"name": "공백의 힘(추출)",
|
| 254 |
+
"hint": "불필요한 내용을 과감히 제거하라. 빈 공간이 핵심 메시지를 돋보이게 한다.",
|
| 255 |
+
"application": "각 섹션에서 '없어도 되는 문장'을 최소 20% 제거. 핵심만 남겨 밀도 향상."
|
| 256 |
+
}
|
| 257 |
+
],
|
| 258 |
+
"policy_dilemmas": [
|
| 259 |
+
{
|
| 260 |
+
"dilemma": "규제 vs 혁신",
|
| 261 |
+
"resolution": "규제 샌드박스: 한시적 규제 유예로 혁신 실험 허용 후 결과 기반 규제 설계",
|
| 262 |
+
"writing_tip": "양측 논리를 균형 있게 제시한 뒤, 제3의 절충안(샌드박스/단계적 도입)을 제안"
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"dilemma": "보안 vs 편의성",
|
| 266 |
+
"resolution": "리스크 기반 인증: 저위험 거래는 간편 인증, 고위험 거래만 강화 인증",
|
| 267 |
+
"writing_tip": "이분법을 피하고 '위험 수준별 차등 접근' 같은 그라데이션 해법을 제시"
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"dilemma": "중앙집중 vs 분산",
|
| 271 |
+
"resolution": "하이브리드 거버넌스: 표준·감독은 중앙, 실행·혁신은 분산",
|
| 272 |
+
"writing_tip": "각 모델의 장단점을 표로 비교한 뒤 결합 모델의 시너지를 논증"
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"dilemma": "성과 vs 형평",
|
| 276 |
+
"resolution": "차등 지원: 성과 기반 인센티브 + 기초 역량 강화 지원 병행",
|
| 277 |
+
"writing_tip": "경쟁과 포용의 균형점을 수치 시뮬레이션으로 제시"
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"dilemma": "단기 vs 장기",
|
| 281 |
+
"resolution": "단계별 로드맵: 단기 Quick-Win으로 추진력 확보, 장기 구조개혁으로 지속성 확보",
|
| 282 |
+
"writing_tip": "로드맵 표를 활용하여 단기(~6개월)/중기(1~2년)/장기(3~5년) 마일스톤 제시"
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"dilemma": "개방 vs 보호",
|
| 286 |
+
"resolution": "조건부 개방: 핵심 기술은 보호하되 생태계 활성화를 위해 주변 기술은 개방",
|
| 287 |
+
"writing_tip": "개방 범위와 보호 범위를 명확히 구분하는 매트릭스 제시"
|
| 288 |
+
}
|
| 289 |
+
],
|
| 290 |
+
"perspective_transforms": [
|
| 291 |
+
{
|
| 292 |
+
"from": "미시(개별 사례)",
|
| 293 |
+
"to": "거시(시스템 전체)",
|
| 294 |
+
"hint": "개별 기업 사례에서 시작하여 산업 전체 트렌드로 확장",
|
| 295 |
+
"writing_tip": "사례 → 패턴 → 시사점 → 정책 제언 순서로 상향 전개"
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"from": "현재(As-Is)",
|
| 299 |
+
"to": "미래(To-Be)",
|
| 300 |
+
"hint": "현재 상태를 진단한 뒤 목표 상태를 선명하게 그리고 갭을 분석",
|
| 301 |
+
"writing_tip": "As-Is/To-Be 비교표 + 갭 분석 + 이행 로드맵 3종 세트"
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"from": "공급자 관점",
|
| 305 |
+
"to": "수요자 관점",
|
| 306 |
+
"hint": "정책 입안자/기업이 아닌 최종 사용자/국민의 시선에서 재해석",
|
| 307 |
+
"writing_tip": "사용자 여정 맵핑으로 Pain Point를 시각화하면 설득력이 배가"
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"from": "정량(숫자)",
|
| 311 |
+
"to": "정성(의미)",
|
| 312 |
+
"hint": "통계 뒤에 숨은 인간적 의미와 사회적 맥락을 함께 서술",
|
| 313 |
+
"writing_tip": "수치 제시 직후 '이것이 의미하는 바는' 문장으로 해석 레이어 추가"
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"from": "국내",
|
| 317 |
+
"to": "글로벌",
|
| 318 |
+
"hint": "한국 상황을 글로벌 벤치마크와 비교하여 위치 파악",
|
| 319 |
+
"writing_tip": "주요국 비교표(미국/EU/일본/한국)로 객관적 포지셔닝 제시"
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
"from": "기술",
|
| 323 |
+
"to": "제도",
|
| 324 |
+
"hint": "기술적 가능성을 제도적 실현가능성과 교차 검토",
|
| 325 |
+
"writing_tip": "기술 성숙도(TRL)와 규제 준비도를 2×2 매트릭스로 동시 평가"
|
| 326 |
+
}
|
| 327 |
+
],
|
| 328 |
+
"metacognitive_protocols": {
|
| 329 |
+
"_desc": "에이전트 간 상호 검증(상극)을 메타인지적 자기검증으로 구현하는 프로토콜",
|
| 330 |
+
"self_reflection": {
|
| 331 |
+
"trigger": "각 에이전트 출력 완료 직전",
|
| 332 |
+
"questions": [
|
| 333 |
+
"내 분석에서 가장 약한 고리(weakest link)는 어디인가?",
|
| 334 |
+
"내가 암묵적으로 가정하고 있는 전제는 무엇인가?",
|
| 335 |
+
"반대 입장에서 보면 어떤 반론이 가능한가?",
|
| 336 |
+
"이 문서를 읽는 비전문가도 핵심을 이해할 수 있는가?"
|
| 337 |
+
]
|
| 338 |
+
},
|
| 339 |
+
"error_recovery": {
|
| 340 |
+
"trigger": "다음 에이전트가 이전 에이전트의 출력을 받을 때",
|
| 341 |
+
"protocol": [
|
| 342 |
+
"이전 에이전트의 주장 중 근거가 불충분한 항목 식별",
|
| 343 |
+
"논리적 비약 또는 자기모순 탐지",
|
| 344 |
+
"누락된 중요 관점 추가 제안",
|
| 345 |
+
"수정 사항을 [수정] 태그로 명시하여 투명성 확보"
|
| 346 |
+
]
|
| 347 |
+
},
|
| 348 |
+
"confidence_calibration": {
|
| 349 |
+
"levels": {
|
| 350 |
+
"HIGH": "3개 이상 독립 출처에서 교차 검증된 팩트",
|
| 351 |
+
"MEDIUM": "1~2개 출처 기반, 논리적으로 타당하나 추가 검증 권장",
|
| 352 |
+
"LOW": "추정·추론 기반, 반드시 '추정' 또는 '가능성' 표현 사용"
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
}
|
engine.dat
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
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
|
index.html
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
| 22 |
+
html,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;}
|
| 59 |
+
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;}
|
| 60 |
+
textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-light);}
|
| 61 |
+
textarea::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;}
|
| 74 |
+
input[type=range]{width:100%;height:4px;-webkit-appearance:none;background:var(--border);border-radius:4px;outline:none;}
|
| 75 |
+
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);}
|
| 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 |
+
@media(max-width:900px){
|
| 135 |
+
.main{grid-template-columns:1fr;grid-template-rows:auto 1fr;}
|
| 136 |
+
.left{max-height:40vh;border-right:none;border-bottom:1px solid var(--border);}
|
| 137 |
+
}
|
| 138 |
+
</style>
|
| 139 |
+
</head>
|
| 140 |
+
<body>
|
| 141 |
+
|
| 142 |
+
<div class="topbar">
|
| 143 |
+
<span class="logo">한지<em>(HANJI)</em></span>
|
| 144 |
+
<span class="topbar-sep"></span>
|
| 145 |
+
<span class="topbar-desc">HWP AI Agent 서���스</span>
|
| 146 |
+
<a class="topbar-url" href="https://hanji.ginigen.ai" target="_blank">🔗 hanji.ginigen.ai</a>
|
| 147 |
+
<span class="topbar-right">
|
| 148 |
+
<a class="topbar-contact" href="mailto:ginigenaihp@gmail.com">📧 문의 · 온프레미스 · 제휴</a>
|
| 149 |
+
</span>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<div class="main">
|
| 153 |
+
<div class="left">
|
| 154 |
+
|
| 155 |
+
<div class="section">
|
| 156 |
+
<div class="label">📌 프롬프트</div>
|
| 157 |
+
<textarea id="prompt" rows="3" placeholder="예: 2026년 AI 보안 유망기업 육성 지원사업 공모 안내문을 작성해주세요."></textarea>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div class="section">
|
| 161 |
+
<div class="label">📎 레퍼런스 문서</div>
|
| 162 |
+
<div class="file-drop" id="fileDrop" onclick="document.getElementById('fileInput').click()">
|
| 163 |
+
<div class="file-icon">📄</div>
|
| 164 |
+
<div class="file-hint">클릭 또는 드래그하여 업로드</div>
|
| 165 |
+
<div class="file-hint" style="margin-top:2px;">HWP · HWPX · PDF · DOCX · TXT</div>
|
| 166 |
+
<div class="file-name" id="fileName"></div>
|
| 167 |
+
</div>
|
| 168 |
+
<input type="file" id="fileInput" accept=".hwp,.hwpx,.hml,.pdf,.docx,.txt,.md,.csv,.json,.xml,.xlsx">
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<div class="section">
|
| 172 |
+
<div class="slider-row">
|
| 173 |
+
<div class="slider-group">
|
| 174 |
+
<div class="slider-label"><span>🔍 검색</span><span class="slider-val" id="valSearch">20</span></div>
|
| 175 |
+
<input type="range" min="5" max="100" step="5" value="20" id="slSearch" oninput="document.getElementById('valSearch').textContent=this.value">
|
| 176 |
+
</div>
|
| 177 |
+
<div class="slider-group">
|
| 178 |
+
<div class="slider-label"><span>🌡 Temp</span><span class="slider-val" id="valTemp">0.6</span></div>
|
| 179 |
+
<input type="range" min="0.1" max="1.0" step="0.05" value="0.6" id="slTemp" oninput="document.getElementById('valTemp').textContent=this.value">
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<div class="section">
|
| 185 |
+
<div class="btn-row">
|
| 186 |
+
<button class="btn btn-primary" id="runBtn" onclick="runPipeline()">
|
| 187 |
+
<span class="spinner" id="btnSpin"></span>
|
| 188 |
+
<span id="btnLabel">🚀 문서 생성</span>
|
| 189 |
+
</button>
|
| 190 |
+
<button class="btn btn-secondary" onclick="stopPipeline()" style="width:44px" title="중지">⛔</button>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="status-bar"><span class="dot status-idle" id="statusDot"></span><span id="statusText">대기 중</span></div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<div class="section">
|
| 196 |
+
<div class="dl-area">
|
| 197 |
+
<div class="label" style="margin-bottom:4px">📥 HWPX 변환</div>
|
| 198 |
+
<div class="dl-fname" id="dlFname">파일 없음</div>
|
| 199 |
+
<div class="btn-row-2">
|
| 200 |
+
<button class="btn btn-primary btn-sm" id="dlBtn" onclick="downloadHwpx()" disabled>📥 HWPX</button>
|
| 201 |
+
<button class="btn btn-secondary btn-sm" onclick="copyDoc()">📋 복사</button>
|
| 202 |
+
</div>
|
| 203 |
+
<div class="dl-status" id="dlStatus"></div>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<!-- Doc Chat — 레퍼런스 문서 기반 QnA -->
|
| 208 |
+
<div class="acc open" id="accChat">
|
| 209 |
+
<div class="acc-header" onclick="toggleAcc('accChat')"><span class="acc-arrow">▶</span> 💬 문서 QnA 챗</div>
|
| 210 |
+
<div class="acc-body">
|
| 211 |
+
<div id="chatDocStatus" style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-bottom:6px;">📎 레퍼런스 문서를 업로드하면 자동 연동됩니다</div>
|
| 212 |
+
<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>
|
| 213 |
+
<div style="display:grid;grid-template-columns:1fr auto;gap:6px;">
|
| 214 |
+
<input type="text" id="chatInput" placeholder="문서에 대해 질문하세요..." style="font-size:12px;padding:8px 10px;" onkeydown="if(event.key==='Enter')sendChat()">
|
| 215 |
+
<button class="btn btn-primary btn-sm" onclick="sendChat()" style="width:40px;">🚀</button>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<div class="acc" id="accDoc">
|
| 221 |
+
<div class="acc-header" onclick="toggleAcc('accDoc')"><span class="acc-arrow">▶</span> 📝 생성된 텍스트</div>
|
| 222 |
+
<div class="acc-body"><textarea id="docArea" rows="10" placeholder="문서 생성 후 표시됩니다"></textarea></div>
|
| 223 |
+
</div>
|
| 224 |
+
<div class="acc" id="accStream">
|
| 225 |
+
<div class="acc-header" onclick="toggleAcc('accStream')"><span class="acc-arrow">▶</span> ⚡ 에이전트 스트림</div>
|
| 226 |
+
<div class="acc-body"><textarea id="streamArea" rows="8" readonly placeholder="에이전트 실시간 출력"></textarea></div>
|
| 227 |
+
</div>
|
| 228 |
+
<div class="acc" id="accLog">
|
| 229 |
+
<div class="acc-header" onclick="toggleAcc('accLog')"><span class="acc-arrow">▶</span> 🧬 파이프라인 로그</div>
|
| 230 |
+
<div class="acc-body"><textarea id="logArea" rows="6" readonly placeholder="파이프라인 로그"></textarea></div>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<div class="right">
|
| 235 |
+
<div class="vt">
|
| 236 |
+
<div class="vt-dots"><span style="background:#ff5f57"></span><span style="background:#febc2e"></span><span style="background:#28c840"></span></div>
|
| 237 |
+
<span class="vt-fname" id="viewerFname">📄 sample_hanji.hwpx</span>
|
| 238 |
+
<span class="vt-badge badge-e" id="badgeEngine">Python lxml</span>
|
| 239 |
+
<span class="vt-badge badge-r" id="badgeRender">HWPX · Full Render v3</span>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="vb" id="viewerBody">
|
| 242 |
+
<div class="ve"><div class="ve-icon">⏳</div><div class="ve-text">로딩 중...</div></div>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
|
| 247 |
+
<div class="toast" id="toast"></div>
|
| 248 |
+
<a id="dlLink" style="display:none"></a>
|
| 249 |
+
|
| 250 |
+
<script>
|
| 251 |
+
let running=false,finalDoc='',sc=0;
|
| 252 |
+
function getBase(){return '';}
|
| 253 |
+
|
| 254 |
+
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);}
|
| 255 |
+
function toggleAcc(id){document.getElementById(id).classList.toggle('open');}
|
| 256 |
+
function setStatus(t,s='idle'){document.getElementById('statusText').textContent=t;document.getElementById('statusDot').className='dot status-'+s;}
|
| 257 |
+
|
| 258 |
+
const fi=document.getElementById('fileInput'),fd=document.getElementById('fileDrop'),fn=document.getElementById('fileName');
|
| 259 |
+
fi.addEventListener('change',function(){if(this.files.length)handleFile(this.files[0]);});
|
| 260 |
+
fd.addEventListener('dragover',e=>{e.preventDefault();fd.style.borderColor='var(--accent)';});
|
| 261 |
+
fd.addEventListener('dragleave',()=>{fd.style.borderColor='';});
|
| 262 |
+
fd.addEventListener('drop',e=>{e.preventDefault();fd.style.borderColor='';if(e.dataTransfer.files.length)handleFile(e.dataTransfer.files[0]);});
|
| 263 |
+
|
| 264 |
+
function handleFile(f){
|
| 265 |
+
fn.textContent='✅ '+f.name+' ('+Math.round(f.size/1024)+'KB)';fn.style.display='block';fd.classList.add('has-file');
|
| 266 |
+
// 챗용 텍스트 추출 동시 수행
|
| 267 |
+
uploadDocForChat(f);
|
| 268 |
+
// HWP/HWPX → 뷰어 미리보기
|
| 269 |
+
const ext=f.name.split('.').pop().toLowerCase();
|
| 270 |
+
if(['hwp','hwpx'].includes(ext)){
|
| 271 |
+
const r=new FileReader();
|
| 272 |
+
r.onload=function(){renderB64(r.result.split(',')[1],'.'+ext,f.name);};
|
| 273 |
+
r.readAsDataURL(f);
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
function setEmpty(icon,msg){document.getElementById('viewerBody').innerHTML='<div class="ve"><div class="ve-icon">'+icon+'</div><div class="ve-text">'+msg+'</div></div>';}
|
| 278 |
+
|
| 279 |
+
async function renderPath(path,title){
|
| 280 |
+
setEmpty('⏳','렌더링 중...');
|
| 281 |
+
document.getElementById('viewerFname').textContent='📄 '+title;
|
| 282 |
+
const b=document.getElementById('badgeRender');b.className='vt-badge badge-l';b.textContent='로딩...';
|
| 283 |
+
try{
|
| 284 |
+
const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:path})});
|
| 285 |
+
if(!r.ok)throw new Error(r.status);applyHTML(await r.text());
|
| 286 |
+
}catch(e){setEmpty('❌',e.message);b.className='vt-badge badge-e';b.textContent='Error';}
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
async function renderB64(b64,ext,title){
|
| 290 |
+
setEmpty('⏳','렌더링 중...');
|
| 291 |
+
document.getElementById('viewerFname').textContent='📄 '+title;
|
| 292 |
+
const b=document.getElementById('badgeRender');b.className='vt-badge badge-l';b.textContent='로딩...';
|
| 293 |
+
try{
|
| 294 |
+
const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({b64,ext})});
|
| 295 |
+
if(!r.ok)throw new Error(r.status);applyHTML(await r.text());
|
| 296 |
+
}catch(e){setEmpty('❌',e.message);b.className='vt-badge badge-e';b.textContent='Error';}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
function applyHTML(h){
|
| 300 |
+
const v=document.getElementById('viewerBody');
|
| 301 |
+
if(h.includes('srcdoc=')){v.innerHTML=h;}
|
| 302 |
+
else{v.innerHTML='<div style="position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;padding:16px;overflow:auto;">'+h+'</div>';}
|
| 303 |
+
const b=document.getElementById('badgeRender');
|
| 304 |
+
b.className='vt-badge badge-r';b.textContent=h.includes('ohah/hwpjs')?'✨ ohah/hwpjs WASM':'HWPX · Full Render v3';
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
async function runPipeline(){
|
| 308 |
+
const p=document.getElementById('prompt').value.trim();
|
| 309 |
+
if(!p){showToast('프롬프트를 입력하세요.','err');return;}
|
| 310 |
+
running=true;finalDoc='';sc=0;
|
| 311 |
+
document.getElementById('runBtn').classList.add('running');
|
| 312 |
+
document.getElementById('btnLabel').textContent='생성 중...';
|
| 313 |
+
document.getElementById('dlBtn').disabled=true;
|
| 314 |
+
setStatus('파이프라인 실행 중...','run');
|
| 315 |
+
document.getElementById('accStream').classList.add('open');
|
| 316 |
+
try{
|
| 317 |
+
const r=await fetch(getBase()+'/soma/run',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 318 |
+
body:JSON.stringify({prompt:p,max_search:+document.getElementById('slSearch').value,temperature:+document.getElementById('slTemp').value,ref_sid:chatSid||''})});
|
| 319 |
+
if(!r.ok)throw new Error(await r.text());
|
| 320 |
+
const rd=r.body.getReader();const dec=new TextDecoder();let buf='',stream='',log='';
|
| 321 |
+
while(true){
|
| 322 |
+
const{done,value}=await rd.read();if(done||!running)break;
|
| 323 |
+
buf+=dec.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop();
|
| 324 |
+
for(const l of lines){
|
| 325 |
+
if(!l.startsWith('data: '))continue;const d=l.slice(6);
|
| 326 |
+
if(d==='[DONE]'){running=false;break;}
|
| 327 |
+
try{const c=JSON.parse(d);
|
| 328 |
+
if(c.stream){stream+=c.stream;document.getElementById('streamArea').value=stream.slice(-4000);}
|
| 329 |
+
if(c.log){document.getElementById('logArea').value=c.log;}
|
| 330 |
+
if(c.search_count!=null){sc=c.search_count;setStatus('🔍 '+sc+'회 검색...','run');}
|
| 331 |
+
if(c.final_doc){finalDoc=c.final_doc;document.getElementById('docArea').value=finalDoc;}
|
| 332 |
+
if(c.done){finalDoc=c.final_doc||finalDoc;document.getElementById('docArea').value=finalDoc;}
|
| 333 |
+
}catch{}
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
document.getElementById('runBtn').classList.remove('running');
|
| 337 |
+
document.getElementById('btnLabel').textContent='🚀 문서 생성';
|
| 338 |
+
document.getElementById('dlBtn').disabled=!finalDoc;
|
| 339 |
+
if(finalDoc){setStatus('✅ 완료 · '+sc+'회 검색','done');showToast('✅ 문서 생성 완료!','ok');document.getElementById('accDoc').classList.add('open');}
|
| 340 |
+
else setStatus('완료 (문서 없음)','idle');
|
| 341 |
+
}catch(e){setStatus('❌ '+e.message,'err');showToast('❌ '+e.message,'err');
|
| 342 |
+
document.getElementById('runBtn').classList.remove('running');document.getElementById('btnLabel').textContent='🚀 문서 생성';}
|
| 343 |
+
running=false;
|
| 344 |
+
}
|
| 345 |
+
function stopPipeline(){running=false;setStatus('⛔ 중지됨','idle');document.getElementById('runBtn').classList.remove('running');document.getElementById('btnLabel').textContent='🚀 문서 생성';showToast('⛔ 중지됨');}
|
| 346 |
+
|
| 347 |
+
async function downloadHwpx(){
|
| 348 |
+
if(!finalDoc){showToast('문서를 먼저 생성하세요.','err');return;}
|
| 349 |
+
document.getElementById('dlStatus').className='dl-status';document.getElementById('dlStatus').textContent='변환 중...';
|
| 350 |
+
try{
|
| 351 |
+
const r=await fetch(getBase()+'/soma/hml',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:finalDoc})});
|
| 352 |
+
if(!r.ok)throw new Error(await r.text());const d=await r.json();
|
| 353 |
+
if(d.file_url){
|
| 354 |
+
const a=document.getElementById('dlLink');a.href=getBase()+d.file_url;a.download=d.filename||'문서.hwpx';a.click();
|
| 355 |
+
document.getElementById('dlFname').textContent=d.filename||'문서.hwpx';
|
| 356 |
+
document.getElementById('dlStatus').className='dl-status ok';document.getElementById('dlStatus').textContent='✓ 다운로드 완료';
|
| 357 |
+
showToast('✅ HWPX 다운로드','ok');
|
| 358 |
+
if(d.file_path)renderPath(d.file_path,d.filename||'문서.hwpx');
|
| 359 |
+
}else throw new Error('file_url 없음');
|
| 360 |
+
}catch(e){document.getElementById('dlStatus').className='dl-status err';document.getElementById('dlStatus').textContent='⚠ '+e.message;showToast('❌ '+e.message,'err');}
|
| 361 |
+
}
|
| 362 |
+
/* ── SIMPLE MD → HTML ── */
|
| 363 |
+
function md2html(s){
|
| 364 |
+
return s
|
| 365 |
+
.replace(/&/g,'&').replace(/</g,'<')
|
| 366 |
+
.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>')
|
| 367 |
+
.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>')
|
| 368 |
+
.replace(/^# (.+)$/gm,'<div style="font-weight:900;font-size:14px;color:#0f172a;margin:12px 0 6px;">$1</div>')
|
| 369 |
+
.replace(/\*\*(.+?)\*\*/g,'<b>$1</b>')
|
| 370 |
+
.replace(/\*(.+?)\*/g,'<i>$1</i>')
|
| 371 |
+
.replace(/`([^`]+)`/g,'<code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;font-size:10px;color:#6366f1;">$1</code>')
|
| 372 |
+
.replace(/^- (.+)$/gm,'<div style="padding-left:14px;text-indent:-10px;margin:2px 0;">• $1</div>')
|
| 373 |
+
.replace(/^\d+\. (.+)$/gm,function(m,p1){return '<div style="padding-left:14px;margin:2px 0;">'+m.match(/^\d+/)[0]+'. '+p1+'</div>';})
|
| 374 |
+
.replace(/\n{2,}/g,'<div style="margin:6px 0;"></div>')
|
| 375 |
+
.replace(/\n/g,'<br>');
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
function copyDoc(){const t=document.getElementById('docArea').value;if(!t){showToast('텍스트 없음','err');return;}navigator.clipboard.writeText(t).then(()=>showToast('📋 복사됨','ok'));}
|
| 379 |
+
|
| 380 |
+
/* ── DOC CHAT ── */
|
| 381 |
+
let chatSid='',chatHistory=[];
|
| 382 |
+
|
| 383 |
+
function addChatMsg(role,text){
|
| 384 |
+
const box=document.getElementById('chatMessages');
|
| 385 |
+
const color=role==='user'?'var(--accent)':role==='system'?'var(--muted)':'var(--teal)';
|
| 386 |
+
const label=role==='user'?'👤':role==='system'?'ℹ️':'🤖';
|
| 387 |
+
const rendered=role==='user'?text.replace(/\n/g,'<br>'):md2html(text);
|
| 388 |
+
box.innerHTML+='<div style="margin-bottom:6px;"><span style="font-weight:700;color:'+color+';font-size:10px;">'+label+'</span> <span>'+rendered+'</span></div>';
|
| 389 |
+
box.scrollTop=box.scrollHeight;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
async function uploadDocForChat(file){
|
| 393 |
+
/* 레퍼런스 업로드 시 자동 호출 — 챗용 텍스트 추출 */
|
| 394 |
+
try{
|
| 395 |
+
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);});
|
| 396 |
+
const ext='.'+file.name.split('.').pop().toLowerCase();
|
| 397 |
+
const resp=await fetch(getBase()+'/soma/doc-upload',{method:'POST',
|
| 398 |
+
headers:{'Content-Type':'application/json'},body:JSON.stringify({b64:b64,filename:file.name,ext:ext})});
|
| 399 |
+
const d=await resp.json();
|
| 400 |
+
if(d.ok){
|
| 401 |
+
chatSid=d.sid;chatHistory=[];
|
| 402 |
+
document.getElementById('chatDocStatus').innerHTML='✅ <b>'+file.name+'</b> ('+d.chars.toLocaleString()+'자) 연동됨';
|
| 403 |
+
document.getElementById('chatMessages').innerHTML='';
|
| 404 |
+
addChatMsg('system','📄 문서 로드 완료 ('+d.chars.toLocaleString()+'자). 질문하세요!');
|
| 405 |
+
}
|
| 406 |
+
}catch(e){
|
| 407 |
+
document.getElementById('chatDocStatus').textContent='❌ 문서 분석 실패: '+e.message;
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
async function sendChat(){
|
| 412 |
+
var input=document.getElementById('chatInput');
|
| 413 |
+
var msg=input.value.trim();
|
| 414 |
+
if(!msg){return;}
|
| 415 |
+
input.value='';
|
| 416 |
+
addChatMsg('user',msg);
|
| 417 |
+
chatHistory.push([msg,'']);
|
| 418 |
+
|
| 419 |
+
try{
|
| 420 |
+
var r=await fetch(getBase()+'/soma/chat',{method:'POST',
|
| 421 |
+
headers:{'Content-Type':'application/json'},
|
| 422 |
+
body:JSON.stringify({message:msg,sid:chatSid,history:chatHistory.slice(-6)})});
|
| 423 |
+
if(!r.ok){throw new Error('서버 오류 '+r.status);}
|
| 424 |
+
var rd=r.body.getReader();
|
| 425 |
+
var dec=new TextDecoder();
|
| 426 |
+
var buf='',full='';
|
| 427 |
+
var box=document.getElementById('chatMessages');
|
| 428 |
+
var assistDiv=document.createElement('div');
|
| 429 |
+
assistDiv.style.marginBottom='6px';
|
| 430 |
+
assistDiv.innerHTML='<span style="font-weight:700;color:var(--teal);font-size:10px;">🤖</span> <span id="chatStream"></span>';
|
| 431 |
+
box.appendChild(assistDiv);
|
| 432 |
+
var streamSpan=document.getElementById('chatStream');
|
| 433 |
+
|
| 434 |
+
while(true){
|
| 435 |
+
var result=await rd.read();
|
| 436 |
+
if(result.done){break;}
|
| 437 |
+
buf+=dec.decode(result.value,{stream:true});
|
| 438 |
+
var lines=buf.split('\n');
|
| 439 |
+
buf=lines.pop();
|
| 440 |
+
for(var i=0;i<lines.length;i++){
|
| 441 |
+
var l=lines[i];
|
| 442 |
+
if(l.indexOf('data: ')!==0){continue;}
|
| 443 |
+
var d=l.slice(6);
|
| 444 |
+
if(d==='[DONE]'){break;}
|
| 445 |
+
try{var c=JSON.parse(d);if(c.delta){full+=c.delta;streamSpan.innerHTML=md2html(full);}}catch(e2){}
|
| 446 |
+
}
|
| 447 |
+
box.scrollTop=box.scrollHeight;
|
| 448 |
+
}
|
| 449 |
+
// Remove temp id
|
| 450 |
+
streamSpan.removeAttribute('id');
|
| 451 |
+
chatHistory[chatHistory.length-1][1]=full;
|
| 452 |
+
}catch(e){addChatMsg('system','❌ '+e.message);}
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
window.addEventListener('DOMContentLoaded',async()=>{
|
| 456 |
+
try{const r=await fetch(getBase()+'/soma/status');const s=await r.json();document.getElementById('badgeEngine').textContent=s.engine||'Python lxml';}catch{}
|
| 457 |
+
try{
|
| 458 |
+
const r=await fetch(getBase()+'/soma/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:'/app/sample_hanji.hwpx'})});
|
| 459 |
+
if(r.ok){applyHTML(await r.text());document.getElementById('viewerFname').textContent='📄 한지(HANJI) 서비스 소개';}
|
| 460 |
+
else setEmpty('📄','문서를 생성하거나 레퍼런스를 업로드하세요.');
|
| 461 |
+
}catch{setEmpty('📄','문서를 생성하거나 레퍼런스를 업로드하세요.');}
|
| 462 |
+
// 샘플 문서 챗 자동 연동
|
| 463 |
+
try{
|
| 464 |
+
var cr=await fetch(getBase()+'/soma/doc-upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_path:'/app/sample_hanji.hwpx'})});
|
| 465 |
+
var cd=await cr.json();
|
| 466 |
+
if(cd.ok){chatSid=cd.sid;document.getElementById('chatDocStatus').innerHTML='✅ <b>한지 서비스 소개</b> ('+cd.chars.toLocaleString()+'자) 연동됨';addChatMsg('system','📄 기본 문서 로드 완료. 한지(HANJI)에 대해 질문하세요!');}
|
| 467 |
+
}catch{}
|
| 468 |
+
});
|
| 469 |
+
</script>
|
| 470 |
+
</body>
|
| 471 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
huggingface_hub
|
| 2 |
+
requests
|
| 3 |
+
fastapi
|
| 4 |
+
uvicorn[standard]
|
| 5 |
+
gradio
|
| 6 |
+
olefile
|
| 7 |
+
pyhwp
|
| 8 |
+
PyPDF2
|
| 9 |
+
pdfplumber
|
| 10 |
+
openpyxl
|
| 11 |
+
lxml
|
sample_hanji.hwpx
ADDED
|
Binary file (68.8 kB). View file
|
|
|