// JGOS — 전남광주 통합특별시 시민 AI 포털 (UI/UX v2) const $ = (s) => document.querySelector(s); const $$ = (s) => document.querySelectorAll(s); const messagesEl = $("#messages"); const portalEl = $("#portal"); const inputEl = $("#input"); const sendBtn = $("#send-btn"); const attachBtn = $("#attach-btn"); const webBtn = $("#web-btn"); const fileInput = $("#file-input"); const attachRow = $("#attach-row"); const chatListEl = $("#chat-list"); const newChatBtn = $("#new-chat-btn"); const warnBar = $("#warn-bar"); const statusEl = $("#status"); const dragOverlay = $("#drag-overlay"); const sidebarEl = $("#sidebar"); const sidebarBackdrop = $("#sidebar-backdrop"); const hamburgerBtn = $("#hamburger-btn"); const sidebarToggle = $("#sidebar-toggle"); // marked.js if (window.marked) { marked.setOptions({ gfm: true, breaks: true, highlight: (code, lang) => { if (window.hljs && lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch (e) {} } return window.hljs ? hljs.highlightAuto(code).value : code; }, }); } let chats = JSON.parse(localStorage.getItem("jgos_chats") || "[]"); let activeChatId = null; let isStreaming = false; let pendingAttachments = []; let webSearchOn = false; // 🔍 웹검색 토글 (네이버+웹 실시간 grounding) const LEADER_KEYWORDS = ["시장", "구청장", "군수", "도지사", "단체장", "당선", "임기"]; const ALLOWED_EXTS = ["hwp", "hwpx", "pdf", "docx", "pptx", "xlsx", "xls", "csv", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"]; function uuid() { return Math.random().toString(36).slice(2, 10) + Date.now().toString(36); } function escHtml(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); } function renderSources(srcs) { if (!srcs || !srcs.length) return ""; const items = srcs.slice(0, 8).map((s, i) => `` + `${i + 1}` + `${escHtml(s.title)}` + `${escHtml(s.src)}` ).join(""); return `
\uBBF8\uB9AC\uBCF4\uAE30 \uC2E4\uD328 (" + r.status + ")
"; return; } const d = await r.json(); body.innerHTML = (d.rows > 0) ? (d.html || "\uBBF8\uB9AC\uBCF4\uAE30 \uC5C6\uC74C
") : "\u26A0\uFE0F \uD30C\uC2F1 \uC2E4\uD328
"; } catch(e) { body.innerHTML = "\uC624\uB958: " + (e && e.message ? e.message : e) + "
"; } } // 모달 닫기 const axModalClose = $("#ax-modal-close"); const axModal = $("#ax-modal"); if (axModalClose) axModalClose.onclick = () => { if (axModal) axModal.hidden = true; }; if (axModal) axModal.onclick = (e) => { if (e.target === axModal) axModal.hidden = true; }; // 파일 chip 렌더링 renderFileChips("ax-excel-demos", DEMO_FILES_EXCEL); renderFileChips("ax-format-demos", DEMO_FILES_FORMAT); // ── Demo 1: Excel 통합 ── const axExcelInput = $("#ax-excel-input"); const axExcelDrop = $("#ax-excel-drop"); const axExcelRun = $("#ax-excel-run"); const axExcelCount = $("#ax-excel-count"); const axExcelResult = $("#ax-excel-result"); let axExcelFiles = []; function axExcelUpdateUI() { if (axExcelCount) axExcelCount.textContent = axExcelFiles.length ? `${axExcelFiles.length}개 파일 선택됨` : ""; if (axExcelRun) axExcelRun.disabled = axExcelFiles.length === 0; } if (axExcelDrop) { axExcelDrop.onclick = (e) => { if (e.target === axExcelDrop || axExcelDrop.contains(e.target)) axExcelInput && axExcelInput.click(); }; axExcelDrop.ondragover = e => { e.preventDefault(); axExcelDrop.classList.add("dragover"); }; axExcelDrop.ondragleave = () => axExcelDrop.classList.remove("dragover"); axExcelDrop.ondrop = e => { e.preventDefault(); axExcelDrop.classList.remove("dragover"); axExcelFiles = [...axExcelFiles, ...(e.dataTransfer.files || [])]; axExcelUpdateUI(); }; } if (axExcelInput) axExcelInput.onchange = () => { axExcelFiles = [...axExcelFiles, ...(axExcelInput.files || [])]; axExcelUpdateUI(); }; if (axExcelRun) axExcelRun.onclick = async () => { if (!axExcelFiles.length) return; axExcelRun.disabled = true; axExcelRun.innerHTML = "⏳ 처리 중..."; if (axExcelResult) { axExcelResult.hidden = false; axExcelResult.innerHTML = '오류: ${e.message}
`; } axExcelRun.disabled = false; axExcelRun.innerHTML = "⚡ 통합 실행"; }; // ── Demo 2: 이종 포맷 통합 ── const axFormatInput = $("#ax-format-input"); const axFormatDrop = $("#ax-format-drop"); const axFormatRun = $("#ax-format-run"); const axFormatCount = $("#ax-format-count"); const axFormatResult = $("#ax-format-result"); let axFormatFiles = []; function axFormatUpdateUI() { if (axFormatCount) axFormatCount.textContent = axFormatFiles.length ? `${axFormatFiles.length}개 파일 선택됨` : ""; if (axFormatRun) axFormatRun.disabled = axFormatFiles.length === 0; } if (axFormatDrop) { axFormatDrop.onclick = (e) => { if (e.target === axFormatDrop || axFormatDrop.contains(e.target)) axFormatInput && axFormatInput.click(); }; axFormatDrop.ondragover = e => { e.preventDefault(); axFormatDrop.classList.add("dragover"); }; axFormatDrop.ondragleave = () => axFormatDrop.classList.remove("dragover"); axFormatDrop.ondrop = e => { e.preventDefault(); axFormatDrop.classList.remove("dragover"); axFormatFiles = [...axFormatFiles, ...(e.dataTransfer.files || [])]; axFormatUpdateUI(); }; } if (axFormatInput) axFormatInput.onchange = () => { axFormatFiles = [...axFormatFiles, ...(axFormatInput.files || [])]; axFormatUpdateUI(); }; if (axFormatRun) axFormatRun.onclick = async () => { if (!axFormatFiles.length) return; axFormatRun.disabled = true; axFormatRun.innerHTML = "⏳ 처리 중..."; if (axFormatResult) { axFormatResult.hidden = false; axFormatResult.innerHTML = '오류: ${e.message}
`; } axFormatRun.disabled = false; axFormatRun.innerHTML = "⚡ 통합 실행"; }; // ===== health ===== fetch("/api/health").then((r) => r.json()).then((d) => { statusEl.textContent = "● " + ("JGOS-31B-Citizen"); }).catch(() => { statusEl.textContent = "● 연결 안 됨"; statusEl.classList.add("error"); }); // ===== init ===== if (chats.length === 0) newChat(); else loadChat(chats[chats.length - 1].id);