JGOS-Origin / static /script.js
openfree's picture
fix: ax preview use GET /api/demo/preview - single fetch, no FormData/blob
e14299d verified
Raw
History Blame Contribute Delete
39.9 kB
// 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c])); }
function renderSources(srcs) {
if (!srcs || !srcs.length) return "";
const items = srcs.slice(0, 8).map((s, i) =>
`<a class="src-card" href="${escHtml(s.link)}" target="_blank" rel="noopener noreferrer">` +
`<span class="src-num">${i + 1}</span>` +
`<span class="src-body"><span class="src-title">${escHtml(s.title)}</span>` +
`<span class="src-tag">${escHtml(s.src)}</span></span></a>`
).join("");
return `<div class="src-wrap"><div class="src-head">๐Ÿ” ์ฐธ๊ณ ํ•œ ์›น ์ถœ์ฒ˜</div><div class="src-list">${items}</div></div>`;
}
function saveChats() {
// vision base64 image_url์€ localStorage์— ์ €์žฅํ•˜์ง€ ์•Š์Œ (quota ์ดˆ๊ณผ ๋ฐฉ์ง€) โ€” ๋ฉ”๋ชจ๋ฆฌ์—” ์œ ์ง€(์ „์†ก์šฉ)
const slim = () => chats.map((c) => ({
...c,
messages: c.messages.map((m) => {
if (Array.isArray(m.content)) {
return { ...m, content: m.content.map((it) =>
it && it.type === "image_url"
? { type: "image_url", image_url: { url: "[stored-image]" } }
: it
) };
}
return m;
}),
}));
try {
localStorage.setItem("jgos_chats", JSON.stringify(slim()));
} catch (e) {
// quota ์ดˆ๊ณผ โ€” ์ตœ๊ทผ 8๊ฐœ ๋Œ€ํ™”๋งŒ ์œ ์ง€ํ•˜๊ณ  ์žฌ์‹œ๋„
try {
chats = chats.slice(-8);
localStorage.setItem("jgos_chats", JSON.stringify(slim()));
} catch (e2) {
console.warn("saveChats failed (quota):", e2); // ์ €์žฅ ์‹คํŒจํ•ด๋„ ๋Œ€ํ™”๋Š” ๊ณ„์†
}
}
}
function todayKR() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
function fileSizeLabel(kb) {
return kb >= 1024 ? `${(kb/1024).toFixed(1)}MB` : `${kb}KB`;
}
// LaTeX ์ˆ˜์‹ โ†’ ์œ ๋‹ˆ์ฝ”๋“œ (๋ชจ๋ธ์ด $\rightarrow$ ๊ฐ™์€ ํ‘œ๊ธฐ ์ถœ๋ ฅ ์‹œ ์‚ฌ๋žŒ์ด ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ)
const LATEX_MAP = {
"rightarrow": "โ†’", "to": "โ†’", "Rightarrow": "โ‡’", "longrightarrow": "โ†’",
"leftarrow": "โ†", "Leftarrow": "โ‡", "leftrightarrow": "โ†”", "Leftrightarrow": "โ‡”",
"uparrow": "โ†‘", "downarrow": "โ†“",
"times": "ร—", "div": "รท", "pm": "ยฑ", "mp": "โˆ“",
"cdot": "ยท", "bullet": "โ€ข", "circ": "โˆ˜", "ast": "โˆ—",
"approx": "โ‰ˆ", "sim": "โˆผ", "simeq": "โ‰ƒ", "cong": "โ‰…", "equiv": "โ‰ก",
"le": "โ‰ค", "leq": "โ‰ค", "ge": "โ‰ฅ", "geq": "โ‰ฅ", "neq": "โ‰ ", "ne": "โ‰ ",
"ll": "โ‰ช", "gg": "โ‰ซ", "subset": "โŠ‚", "supset": "โŠƒ", "subseteq": "โІ", "supseteq": "โЇ",
"in": "โˆˆ", "notin": "โˆ‰", "cap": "โˆฉ", "cup": "โˆช", "emptyset": "โˆ…",
"infty": "โˆž", "partial": "โˆ‚", "nabla": "โˆ‡", "forall": "โˆ€", "exists": "โˆƒ",
"sum": "โˆ‘", "prod": "โˆ", "int": "โˆซ", "sqrt": "โˆš",
"alpha": "ฮฑ", "beta": "ฮฒ", "gamma": "ฮณ", "delta": "ฮด", "epsilon": "ฮต", "varepsilon": "ฮต",
"zeta": "ฮถ", "eta": "ฮท", "theta": "ฮธ", "vartheta": "ฯ‘", "iota": "ฮน", "kappa": "ฮบ",
"lambda": "ฮป", "mu": "ฮผ", "nu": "ฮฝ", "xi": "ฮพ", "pi": "ฯ€", "varpi": "ฯ–",
"rho": "ฯ", "varrho": "ฯฑ", "sigma": "ฯƒ", "varsigma": "ฯ‚", "tau": "ฯ„",
"upsilon": "ฯ…", "phi": "ฯ†", "varphi": "ฯ•", "chi": "ฯ‡", "psi": "ฯˆ", "omega": "ฯ‰",
"Gamma": "ฮ“", "Delta": "ฮ”", "Theta": "ฮ˜", "Lambda": "ฮ›", "Xi": "ฮž", "Pi": "ฮ ",
"Sigma": "ฮฃ", "Upsilon": "ฮฅ", "Phi": "ฮฆ", "Psi": "ฮจ", "Omega": "ฮฉ",
"degree": "ยฐ", "prime": "โ€ฒ", "dprime": "โ€ณ",
"ldots": "โ€ฆ", "cdots": "โ‹ฏ", "vdots": "โ‹ฎ", "ddots": "โ‹ฑ",
"%": "%", "&": "&", "_": "_", "#": "#",
};
function deLatex(text) {
if (!text || typeof text !== "string") return text;
// $\command$ โ†’ unicode (๋Œ€๋ถ€๋ถ„์˜ ์ผ€์ด์Šค)
text = text.replace(/\$\\([a-zA-Z]+)\$/g, (m, cmd) => LATEX_MAP[cmd] || m);
// \(...\) inline math, \[...\] display math ๋‚ด๋ถ€์—์„œ๋„ ๋‹จ์ผ ๋ช…๋ น์–ด ๋ณ€ํ™˜
text = text.replace(/\\\(\s*\\([a-zA-Z]+)\s*\\\)/g, (m, cmd) => LATEX_MAP[cmd] || m);
text = text.replace(/\\\[\s*\\([a-zA-Z]+)\s*\\\]/g, (m, cmd) => LATEX_MAP[cmd] || m);
// bare \command (math env ๋ฐ–) โ€” ํ•œ๊ตญ์–ด ๋ณธ๋ฌธ์—์„œ ๋นˆ๋ฒˆ
text = text.replace(/\\([a-zA-Z]+)(?![a-zA-Z])/g, (m, cmd) => LATEX_MAP[cmd] || m);
// ๋‹จ์ˆœ $math$ โ€” math ๋‚ด๋ถ€์˜ \rightarrow ๋“ฑ ์ฒ˜๋ฆฌ (์œ„์—์„œ ์ฒ˜๋ฆฌ ๋ชป ํ•œ ์ผ€์ด์Šค)
text = text.replace(/\$([^$\n]{1,30})\$/g, (m, inner) => {
const replaced = inner.replace(/\\([a-zA-Z]+)/g, (mm, cmd) => LATEX_MAP[cmd] || mm);
// ๋ณ€ํ™˜๋๊ณ  LaTeX ํ”์  ์—†์œผ๋ฉด $ ์ œ๊ฑฐ
return /[\\{}]/.test(replaced) ? m : replaced;
});
return text;
}
function renderMarkdown(md) {
if (!md) return "";
md = deLatex(md); // โญ LaTeX โ†’ unicode ๋ณ€ํ™˜
if (window.marked && window.DOMPurify) {
return DOMPurify.sanitize(marked.parse(md));
}
return md.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>");
}
// ===== ํฌํ„ธ ํ‘œ์‹œ/์ˆจ๊น€ =====
function togglePortal(show) {
if (!portalEl) return;
portalEl.style.display = show ? "" : "none";
}
function renderChatList() {
chatListEl.innerHTML = "";
for (const c of chats.slice().reverse()) {
const li = document.createElement("li");
li.textContent = c.title || "์ƒˆ ์ƒ๋‹ด";
li.dataset.id = c.id;
if (c.id === activeChatId) li.classList.add("active");
li.onclick = () => { loadChat(c.id); closeSidebarMobile(); };
chatListEl.appendChild(li);
}
}
function loadChat(id) {
if (isStreaming) { stopStreaming(); isStreaming = false; setSendButtonMode("send"); }
activeChatId = id;
const chat = chats.find((c) => c.id === id);
if (!chat) return;
// _streaming ์ž”์žฌ ์ •๋ฆฌ (์ด์ „์— ์ค‘๋‹จ๋œ ๋ฏธ์™„ ๋ฉ”์‹œ์ง€)
chat.messages = chat.messages.filter(m => !(m._streaming && !m.content));
messagesEl.innerHTML = "";
const userOrAst = chat.messages.filter(m => m.role !== "system");
togglePortal(userOrAst.length === 0);
for (const m of userOrAst) {
// vision content๊ฐ€ list๋ฉด ํ…์ŠคํŠธ ๋ถ€๋ถ„๋งŒ ํ™”๋ฉด์— ํ‘œ์‹œ
let displayContent = m.content;
if (Array.isArray(m.content)) {
const textItem = m.content.find(x => x.type === "text");
const imgCount = m.content.filter(x => x.type === "image_url").length;
displayContent = (textItem?.text || "") +
(imgCount > 0 ? `\n\n๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ${imgCount}์žฅ ์ฒจ๋ถ€` : "");
}
appendMessage(m.role, displayContent, m.attachments || [], { stored: true });
}
renderChatList();
scrollToBottom();
}
function newChat() {
if (isStreaming) { stopStreaming(); isStreaming = false; setSendButtonMode("send"); }
const id = uuid();
const chat = { id, title: "์ƒˆ ์ƒ๋‹ด", messages: [], created: Date.now() };
chats.push(chat);
saveChats();
activeChatId = id;
messagesEl.innerHTML = "";
togglePortal(true); // ์ฒซ ํ™”๋ฉด = ํฌํ„ธ
renderChatList();
inputEl.focus();
}
// ===== ๋ฉ”์‹œ์ง€ ๋ Œ๋”๋ง =====
function buildAttachmentCard(attachments, role) {
if (!attachments || attachments.length === 0) return null;
const card = document.createElement("div");
card.className = "msg-attachments-card";
for (const a of attachments) {
const item = document.createElement("div");
item.className = "attach-card-item";
item.innerHTML = `
<div class="ext-icon">${(a.ext || "?").toUpperCase().slice(0,4)}</div>
<div class="meta">
<div class="name">${escapeHTML(a.name)}</div>
<div class="info">${fileSizeLabel(a.sizeKB)} ยท ${a.status || "์ฒ˜๋ฆฌ์™„๋ฃŒ"}</div>
</div>
<div class="status">โœ“ ๋ถ„์„ ๊ฐ€๋Šฅ</div>
`;
card.appendChild(item);
}
return card;
}
function escapeHTML(s) {
return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function appendMessage(role, content, attachments, opts = {}) {
const div = document.createElement("div");
div.className = `msg ${role}`;
if (content) div.innerHTML = renderMarkdown(content);
if (attachments && attachments.length > 0) {
const card = buildAttachmentCard(attachments, role);
if (card) div.appendChild(card);
}
// assistant ๋ฉ”์‹œ์ง€์— ์‹ ๋ขฐ ๋ฐฐ์ง€ (์ €์žฅ๋œ ๋ฉ”์‹œ์ง€๋ฉด ์ฆ‰์‹œ ์ถ”๊ฐ€, ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฉ”์‹œ์ง€๋Š” ์ข…๋ฃŒ ํ›„ ์ถ”๊ฐ€)
if (role === "assistant" && opts.stored && content) {
attachTrustBadges(div, opts);
}
if (window.hljs) {
div.querySelectorAll("pre code").forEach((b) => { try { hljs.highlightElement(b); } catch (e) {} });
}
messagesEl.appendChild(div);
togglePortal(false);
scrollToBottom();
return div;
}
function attachTrustBadges(asstDiv, opts = {}) {
if (asstDiv.querySelector(".trust-badges")) return;
const bar = document.createElement("div");
bar.className = "trust-badges";
const dayBadge = `<span class="tb gray">๐Ÿ“… ๊ธฐ์ค€์ผ ${todayKR()}</span>`;
const sourceBadge = `<span class="tb">๐Ÿ“š ์ถœ์ฒ˜ RAG</span>`;
const govBadge = `<span class="tb green">๐Ÿ›๏ธ ๊ณต์‹ ์•ˆ๋‚ด 062-120 ํ™•์ธ</span>`;
const electionBadge = `<span class="tb warn">โš ๏ธ ์„ ๊ฑฐยท์ถœ๋ฒ” ์ดํ›„ ๋ณ€๋™ ๊ฐ€๋Šฅ</span>`;
bar.innerHTML = dayBadge + sourceBadge + govBadge + (opts.leader ? electionBadge : "");
asstDiv.appendChild(bar);
}
function scrollToBottom() {
// messages-wrap (์™ธ๋ถ€ ์Šคํฌ๋กค) ์‚ฌ์šฉ
const wrap = document.getElementById("messages-wrap");
if (wrap) wrap.scrollTop = wrap.scrollHeight;
}
function isLeaderQuery(text) {
return LEADER_KEYWORDS.some((k) => text.includes(k));
}
// ===== ํŒŒ์ผ ์—…๋กœ๋“œ =====
attachBtn.onclick = () => fileInput.click();
if (webBtn) webBtn.onclick = () => {
webSearchOn = !webSearchOn;
webBtn.classList.toggle("on", webSearchOn);
webBtn.setAttribute("aria-pressed", webSearchOn ? "true" : "false");
webBtn.title = webSearchOn
? "์›น๊ฒ€์ƒ‰ ์ผœ์ง โ€” ์ตœ์‹  ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค (๋„๋ ค๋ฉด ํด๋ฆญ)"
: "์›น๊ฒ€์ƒ‰ โ€” ์ผœ๋ฉด ๋„ค์ด๋ฒ„ยท์›น์—์„œ ์ตœ์‹  ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค";
};
fileInput.onchange = (e) => { handleFiles(e.target.files); fileInput.value = ""; };
// drag-drop
let dragCounter = 0; let dragTimeout = null;
function showDragOverlay() {
dragOverlay.hidden = false;
if (dragTimeout) clearTimeout(dragTimeout);
dragTimeout = setTimeout(() => { dragCounter = 0; dragOverlay.hidden = true; }, 1500);
}
function hideDragOverlay() {
dragCounter = 0; dragOverlay.hidden = true;
if (dragTimeout) { clearTimeout(dragTimeout); dragTimeout = null; }
}
hideDragOverlay();
document.addEventListener("dragenter", (e) => {
if (!e.dataTransfer || !e.dataTransfer.types.includes("Files")) return;
e.preventDefault(); dragCounter++; showDragOverlay();
});
document.addEventListener("dragover", (e) => {
if (e.dataTransfer && e.dataTransfer.types.includes("Files")) {
e.preventDefault();
if (!dragOverlay.hidden && dragTimeout) {
clearTimeout(dragTimeout);
dragTimeout = setTimeout(() => { dragCounter = 0; dragOverlay.hidden = true; }, 1500);
}
}
});
document.addEventListener("dragleave", () => {
dragCounter--;
if (dragCounter <= 0) hideDragOverlay();
});
document.addEventListener("drop", (e) => {
e.preventDefault();
hideDragOverlay();
if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files);
});
document.addEventListener("dragend", () => hideDragOverlay());
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
if (!dragOverlay.hidden) hideDragOverlay();
if (sidebarEl.classList.contains("open")) closeSidebarMobile();
}
});
dragOverlay.addEventListener("click", hideDragOverlay);
window.addEventListener("blur", hideDragOverlay);
async function handleFiles(files) {
for (const file of files) {
const ext = (file.name.split(".").pop() || "").toLowerCase();
if (!ALLOWED_EXTS.includes(ext)) {
alert(`์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ˜•์‹: .${ext}\n์ง€์›: HWP, HWPX, PDF, DOCX, PPTX, XLSX, XLS, CSV, TXT, MD, ์ด๋ฏธ์ง€`);
continue;
}
const sizeMB = file.size / 1024 / 1024;
if (sizeMB > 30) { alert(`ํŒŒ์ผ ๋„ˆ๋ฌด ํผ: ${file.name} (${sizeMB.toFixed(1)}MB > 30MB)`); continue; }
const atch = {
id: uuid(), name: file.name, ext, sizeKB: Math.round(file.size / 1024),
text: "", status: "uploading",
isImage: ["jpg","jpeg","png","gif","webp"].includes(ext),
imageDataUrl: null,
};
pendingAttachments.push(atch);
renderAttachPills();
const fd = new FormData();
fd.append("file", file);
try {
const r = await fetch("/api/upload", { method: "POST", body: fd });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
atch.text = data.text || "";
atch.meta = data.meta || {};
atch.imageDataUrl = data.image_data_url || null; // โญ vision์šฉ
atch.status = "ready";
} catch (e) {
atch.status = "error";
atch.error = e.message;
}
renderAttachPills();
}
}
function renderAttachPills() {
attachRow.innerHTML = "";
for (const a of pendingAttachments) {
const pill = document.createElement("div");
pill.className = "attach-pill" + (a.status === "uploading" ? " processing" : "");
let icon = "๐Ÿ“Ž";
if (a.status === "uploading") icon = '<span class="spinner"></span>';
if (a.status === "error") icon = "โŒ";
if (a.status === "ready") icon = "โœ…";
pill.innerHTML = `${icon} <span class="ext">${a.ext.toUpperCase()}</span> ${escapeHTML(a.name)} <small style="opacity:0.7">${fileSizeLabel(a.sizeKB)}</small>`;
const x = document.createElement("button");
x.className = "remove"; x.innerHTML = "โœ•"; x.title = "์‚ญ์ œ";
x.onclick = () => {
pendingAttachments = pendingAttachments.filter((p) => p.id !== a.id);
renderAttachPills();
};
pill.appendChild(x);
attachRow.appendChild(pill);
}
}
// ===== send =====
let currentAbort = null; // ์ง„ํ–‰ ์ค‘ ์š”์ฒญ ์ทจ์†Œ์šฉ
let saveThrottle = null; // ๋ถ€๋ถ„ ์ €์žฅ throttle
function stopStreaming() {
if (currentAbort) { try { currentAbort.abort(); } catch (e) {} currentAbort = null; }
}
async function send(presetText) {
// ์ง„ํ–‰ ์ค‘์ด๋ฉด ์ƒˆ ์งˆ๋ฌธ ์ „์— ์ด์ „ ์š”์ฒญ ์ •์ง€ (๋ฒ„๊ทธ: ์งˆ๋ฌธ ์ค‘ ๋‹ค๋ฅธ ์•ก์…˜ ๋ถˆ๊ฐ€ ํ•ด๊ฒฐ)
if (isStreaming) {
stopStreaming();
// abort๊ฐ€ finally๊นŒ์ง€ ๋„๋‹ฌํ•˜๋„๋ก ํ•œ tick ์–‘๋ณด
await new Promise((r) => setTimeout(r, 60));
}
const text = (presetText !== undefined ? presetText : inputEl.value).trim();
if (!text && pendingAttachments.length === 0) return;
if (pendingAttachments.some((a) => a.status === "uploading")) {
alert("ํŒŒ์ผ ์ฒ˜๋ฆฌ ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
return;
}
if (!activeChatId) newChat();
const chat = chats.find((c) => c.id === activeChatId);
const leader = isLeaderQuery(text);
warnBar.hidden = !leader;
const sentAttachments = pendingAttachments.filter((a) => a.status === "ready");
const imageAttachments = sentAttachments.filter((a) => a.isImage && a.imageDataUrl);
const docAttachments = sentAttachments.filter((a) => !a.isImage);
// ๋ณธ๋ฌธ = ๋ฌธ์„œ ํ…์ŠคํŠธ prefix + ์‚ฌ์šฉ์ž ํ…์ŠคํŠธ
let textPart = text;
if (docAttachments.length > 0) {
const docTexts = docAttachments.map((a) =>
`[์ฒจ๋ถ€๋ฌธ์„œ: ${a.name} (${a.ext.toUpperCase()})]\n${a.text || "(์ถ”์ถœ ์‹คํŒจ)"}`
).join("\n\n---\n\n");
textPart = `${docTexts}\n\n---\n\n${text}`;
}
// OpenAI vision format: ์ด๋ฏธ์ง€ ์žˆ์œผ๋ฉด content๋ฅผ list๋กœ
let fullUserContent;
if (imageAttachments.length > 0) {
fullUserContent = [{ type: "text", text: textPart || "์ด ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•ด ์ฃผ์„ธ์š”." }];
for (const img of imageAttachments) {
fullUserContent.push({ type: "image_url", image_url: { url: img.imageDataUrl } });
}
} else {
fullUserContent = textPart;
}
const sentAttMeta = sentAttachments.map((a) => ({
name: a.name, ext: a.ext, sizeKB: a.sizeKB,
status: a.isImage ? "๐Ÿ–ผ๏ธ vision ์ž…๋ ฅ" : "๐Ÿ“„ ํ…์ŠคํŠธ ์ถ”์ถœ",
isImage: a.isImage,
}));
chat.messages.push({ role: "user", content: fullUserContent, attachments: sentAttMeta });
if (chat.messages.length === 1 || chat.title === "์ƒˆ ์ƒ๋‹ด") {
chat.title = (text || sentAttachments[0]?.name || "์ƒˆ ์ƒ๋‹ด").slice(0, 30);
}
// ํ™”๋ฉด ํ‘œ์‹œ๋Š” ํ…์ŠคํŠธ + attachment ์นด๋“œ๋งŒ (base64 ๋ณธ๋ฌธ์€ ์•ˆ ๋ณด์ž„)
const displayText = text || (imageAttachments.length > 0
? `๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ${imageAttachments.length}์žฅ ๋ถ„์„ ์š”์ฒญ`
: `๐Ÿ“Ž ${sentAttachments.length}๊ฐœ ํŒŒ์ผ ์ฒจ๋ถ€`);
appendMessage("user", displayText, sentAttMeta);
inputEl.value = ""; inputEl.style.height = "auto";
pendingAttachments = []; renderAttachPills();
saveChats(); renderChatList();
togglePortal(false);
isStreaming = true;
setSendButtonMode("stop"); // ์ „์†ก ๋ฒ„ํŠผ โ†’ โน ์ •์ง€ ๋ฒ„ํŠผ
const asstDiv = appendMessage("assistant", "", []);
asstDiv.innerHTML = '<span class="thinking-indicator"><span class="spinner"></span> JGOS๊ฐ€ ๋‹ต๋ณ€์„ ์ค€๋น„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹คโ€ฆ</span>';
let acc = "";
let firstTokenReceived = false;
let aborted = false;
let lastSources = null;
// โญ assistant ๋ฉ”์‹œ์ง€๋ฅผ ๋ฏธ๋ฆฌ push (๋ถ€๋ถ„ ์ €์žฅ โ€” ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ๋‹ต๋ณ€ ๋ณด์กด)
const asstMsgIdx = chat.messages.push({ role: "assistant", content: "", _streaming: true }) - 1;
const flushSave = () => {
if (saveThrottle) return;
saveThrottle = setTimeout(() => {
chat.messages[asstMsgIdx].content = acc;
saveChats();
saveThrottle = null;
}, 400);
};
currentAbort = new AbortController();
try {
const resp = await fetch("/api/chat", {
method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" },
signal: currentAbort.signal,
body: JSON.stringify({
messages: chat.messages.filter((m) => m.role !== "system" && !m._streaming),
temperature: 0.3, max_tokens: 2048, web: webSearchOn,
}),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6).trim();
if (data === "[DONE]") continue;
try {
const j = JSON.parse(data);
if (j.error) {
asstDiv.innerHTML = renderMarkdown(`โŒ **์˜ค๋ฅ˜**: ${j.error}`);
statusEl.classList.add("error"); statusEl.textContent = "โ— ์˜ค๋ฅ˜";
throw new Error(j.error);
}
if (j.sources && Array.isArray(j.sources)) { lastSources = j.sources; }
const delta = j.choices?.[0]?.delta?.content || "";
if (delta) {
if (!firstTokenReceived) { firstTokenReceived = true; asstDiv.innerHTML = ""; }
acc += delta;
asstDiv.innerHTML = renderMarkdown(acc) + '<span class="cursor"></span>';
flushSave(); // ๋ถ€๋ถ„ ์ €์žฅ
scrollToBottom();
}
} catch (e) { /* skip non-JSON */ }
}
}
asstDiv.innerHTML = renderMarkdown(acc) + renderSources(lastSources);
if (window.hljs) {
asstDiv.querySelectorAll("pre code").forEach((b) => { try { hljs.highlightElement(b); } catch (e) {} });
}
if (acc) attachTrustBadges(asstDiv, { leader });
} catch (e) {
if (e.name === "AbortError") {
aborted = true;
// ๋ถ€๋ถ„ ๋‹ต๋ณ€ ์œ ์ง€ + ์ค‘๋‹จ ํ‘œ์‹œ
asstDiv.innerHTML = renderMarkdown(acc || "") +
'<div style="margin-top:8px;font-size:12px;color:#8a94a3">โน ๋‹ต๋ณ€์ด ์ค‘๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</div>';
} else {
if (!acc) asstDiv.innerHTML = renderMarkdown(`โŒ **์˜ค๋ฅ˜**: ${e.message || e}`);
statusEl.classList.add("error"); statusEl.textContent = "โ— ์˜ค๋ฅ˜";
}
} finally {
// ์ตœ์ข… ์ €์žฅ (throttle ์ทจ์†Œ + ํ™•์ •)
if (saveThrottle) { clearTimeout(saveThrottle); saveThrottle = null; }
chat.messages[asstMsgIdx].content = acc;
delete chat.messages[asstMsgIdx]._streaming;
if (!acc && aborted) chat.messages.splice(asstMsgIdx, 1); // ๋นˆ ์ค‘๋‹จ์€ ์ œ๊ฑฐ
saveChats();
isStreaming = false; currentAbort = null;
setSendButtonMode("send");
inputEl.focus();
}
}
// ์ „์†ก โ†” ์ •์ง€ ๋ฒ„ํŠผ ํ† ๊ธ€
function setSendButtonMode(mode) {
if (mode === "stop") {
sendBtn.classList.add("stopping");
sendBtn.title = "์ •์ง€";
sendBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
} else {
sendBtn.classList.remove("stopping");
sendBtn.title = "์ „์†ก (Enter)";
sendBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>';
}
}
// ===== ํฌํ„ธ ์นดํ…Œ๊ณ ๋ฆฌ ์นด๋“œ + ์ถ”์ฒœ ์งˆ๋ฌธ ๋ฒ„ํŠผ =====
document.addEventListener("click", (e) => {
// ์นดํ…Œ๊ณ ๋ฆฌ ์นด๋“œ
const catCard = e.target.closest(".cat-card");
if (catCard) {
if (catCard.dataset.attach === "true") {
fileInput.click();
return;
}
if (catCard.dataset.q) {
send(catCard.dataset.q);
return;
}
}
// ์ถ”์ฒœ ์งˆ๋ฌธ ๋ฒ„ํŠผ
const quickBtn = e.target.closest(".quick-btn");
if (quickBtn && quickBtn.dataset.q) {
send(quickBtn.dataset.q);
return;
}
});
// ===== ์ด๋ฒคํŠธ =====
// ์ „์†ก ๋ฒ„ํŠผ: streaming ์ค‘์ด๋ฉด ์ •์ง€, ์•„๋‹ˆ๋ฉด ์ „์†ก
sendBtn.onclick = () => {
if (isStreaming) { stopStreaming(); return; }
send();
};
inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
});
inputEl.addEventListener("input", () => {
inputEl.style.height = "auto";
inputEl.style.height = Math.min(200, inputEl.scrollHeight) + "px";
});
newChatBtn.onclick = newChat;
// ===== ๋ชจ๋ฐ”์ผ ์‚ฌ์ด๋“œ๋ฐ” =====
function openSidebarMobile() {
sidebarEl.classList.add("open");
sidebarBackdrop.hidden = false;
}
function closeSidebarMobile() {
sidebarEl.classList.remove("open");
sidebarBackdrop.hidden = true;
}
if (hamburgerBtn) hamburgerBtn.onclick = openSidebarMobile;
if (sidebarToggle) sidebarToggle.onclick = openSidebarMobile;
if (sidebarBackdrop) sidebarBackdrop.onclick = closeSidebarMobile;
// ===== ์ƒ๋‹จ ํƒญ (๐Ÿ’ฌ AI ์ƒ๋‹ด / ๐Ÿ“‘ ๋ฌด๋ฃŒ ์˜คํ”ผ์Šค / ๐ŸŽจ JGOS-Image / ๐Ÿ›๏ธ ํ–‰์ • AX) =====
const viewChat = $("#view-chat");
const viewOffice = $("#view-office");
const viewImage = $("#view-image");
const viewEdu = $("#view-edu");
const viewAx = $("#view-ax");
const officeFrame = $("#office-frame");
let officeLoaded = false;
function showView(name) {
if (viewChat) viewChat.style.display = name === "chat" ? "" : "none";
if (viewOffice) viewOffice.hidden = name !== "office";
if (viewImage) viewImage.hidden = name !== "image";
if (viewEdu) viewEdu.hidden = name !== "edu";
if (viewAx) viewAx.hidden = name !== "ax";
// ๋ฌด๋ฃŒ ์˜คํ”ผ์Šค๋Š” same-origin iframe(/office) lazy ๋กœ๋“œ (JGOS ํ—ค๋”/ํƒญ ์œ ์ง€)
if (name === "office" && !officeLoaded && officeFrame) { officeFrame.src = "/office/"; officeLoaded = true; }
}
function switchTab(name) {
$$(".top-tab").forEach((t) => t.classList.toggle("active", t.dataset.view === name));
showView(name);
}
$$(".top-tab").forEach((tab) => {
tab.onclick = () => switchTab(tab.dataset.view);
});
// ===== ๐ŸŽจ JGOS-Image ์ด๋ฏธ์ง€ ์ƒ์„ฑ =====
const imgPrompt = $("#img-prompt");
const imgGenBtn = $("#img-gen-btn");
const imgStatus = $("#img-status");
const imgResult = $("#img-result");
const imgRatio = $("#img-ratio");
// ๋น„์œจ โ†’ (width, height). Qwen-Image ๊ถŒ์žฅ ํ•ด์ƒ๋„.
const RATIO_WH = { "1:1": [1024, 1024], "3:4": [896, 1152], "9:16": [768, 1344], "4:3": [1152, 896], "16:9": [1344, 768] };
let imgTimer = null;
$$(".img-ex").forEach((b) => { b.onclick = () => { if (imgPrompt) { imgPrompt.value = b.dataset.p; imgPrompt.focus(); } }; });
async function genImage() {
const prompt = (imgPrompt && imgPrompt.value || "").trim();
if (!prompt) { if (imgPrompt) imgPrompt.focus(); return; }
const [w, h] = RATIO_WH[(imgRatio && imgRatio.value) || "1:1"] || [1024, 1024];
if (imgGenBtn) imgGenBtn.disabled = true;
if (imgResult) imgResult.innerHTML = "";
// ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ ˆ์Šค: ๋ชจ๋ž˜์‹œ๊ณ„ ์• ๋‹ˆ๋ฉ”์ด์…˜ + ๊ฒฝ๊ณผ ์ดˆ ์นด์šดํ„ฐ
let sec = 0;
if (imgStatus) {
imgStatus.hidden = false;
imgStatus.innerHTML = '<span class="img-hourglass">โณ</span> ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆฌ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค โ€” <strong>์•ฝ 10์ดˆ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”</strong> (<b id="img-sec">0</b>์ดˆ)';
}
if (imgTimer) clearInterval(imgTimer);
imgTimer = setInterval(() => { sec++; const e = document.getElementById("img-sec"); if (e) e.textContent = sec; }, 1000);
try {
const r = await fetch("/api/image", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt, width: w, height: h }),
});
const d = await r.json();
if (d.image) {
if (imgResult) imgResult.innerHTML = `<img src="${d.image}" alt="์ƒ์„ฑ ์ด๋ฏธ์ง€" />` +
`<a class="img-dl" href="${d.image}" download="jgos-image.png">โฌ‡ ์ด๋ฏธ์ง€ ์ €์žฅ</a>`;
if (imgStatus) imgStatus.hidden = true;
} else {
if (imgStatus) { imgStatus.hidden = false; imgStatus.textContent = "โš ๏ธ " + (d.error || "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„œ๋ฒ„ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค."); }
}
} catch (e) {
if (imgStatus) { imgStatus.hidden = false; imgStatus.textContent = "โš ๏ธ ์ƒ์„ฑ ์‹คํŒจ: " + e.message; }
} finally {
if (imgTimer) { clearInterval(imgTimer); imgTimer = null; }
if (imgGenBtn) imgGenBtn.disabled = false;
}
}
if (imgGenBtn) imgGenBtn.onclick = genImage;
// ===== ๐ŸŽ“ AI ๊ต์œก โ€” AI ์„ ์ƒ๋‹˜ =====
const eduInput = $("#edu-input");
const eduSend = $("#edu-send");
const eduMessages = $("#edu-messages");
const eduGoto = $("#edu-goto");
const EDU_SYSTEM = "๋‹น์‹ ์€ ์ „๋‚จ๊ด‘์ฃผ ํ†ตํ•ฉํŠน๋ณ„์‹œ 'AI ์‹œ๋ฏผ ๋Œ€ํ•™'์˜ ์นœ์ ˆํ•œ AI ์„ ์ƒ๋‹˜์ž…๋‹ˆ๋‹ค.\nใ€์ ˆ๋Œ€ ๊ทœ์น™ โ€” ์œ„๋ฐ˜ ๊ธˆ์ง€ใ€‘\n1) ๋ฐ˜๋“œ์‹œ ํ•œ๊ตญ์–ด๋กœ๋งŒ ๋‹ตํ•ฉ๋‹ˆ๋‹ค. ์˜์–ด ๋‹จ์–ดยท์˜์–ด ์ •์˜ยท'Technical definition'ยท'Simplified'ยท'Metaphor' ๊ฐ™์€ ์˜์–ด ๋ฉ”๋ชจ๋ฅผ ์ ˆ๋Œ€ ์ถœ๋ ฅํ•˜์ง€ ๋งˆ์„ธ์š”.\n2) ๊ฐœ์š”ยท๊ณ„ํšยท์‚ฌ๊ณ ๊ณผ์ •(๋“ค์—ฌ์“ฐ๊ธฐ ๋ณ„ํ‘œ๋กœ ๋œ ์˜์–ด ์ •๋ฆฌ)์„ ์“ฐ์ง€ ๋ง๊ณ , ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„์„ฑ๋œ ํ•œ๊ตญ์–ด ๋ฌธ์žฅ์œผ๋กœ ๋ฐ”๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.\n3) ๋”ฐ๋œปํ•œ ์กด๋Œ“๋ง, ์‰ฌ์šด ๋น„์œ , ์–ด๋ฅด์‹ ๋„ ์ดํ•ดํ•˜๋„๋ก ์ฒœ์ฒœํžˆ, ์ „๋ฌธ์šฉ์–ด๋Š” ํ’€์–ด์„œ.\n์˜ˆ์‹œ ์‹œ์ž‘) \"์•ˆ๋…•ํ•˜์„ธ์š”! AI๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์‰ฝ๊ฒŒ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”. AI๋Š” ๋งˆ์น˜ ๋˜‘๋˜‘ํ•œ ๋น„์„œ์™€ ๊ฐ™์•„์„œโ€ฆ\"";
const GO_LABEL = { chat: "AI ์ƒ๋‹ด", office: "๋ฌด๋ฃŒ ์˜คํ”ผ์Šค", image: "์ด๋ฏธ์ง€ ์ƒ์„ฑ" };
let eduBusy = false;
function eduCleanup(md) {
// ๋ชจ๋ธ์ด ํ˜๋ฆฌ๋Š” ์˜์–ด ์‚ฌ๊ณ /๊ฐœ์š” ์ค„ ์ œ๊ฑฐ (Technical definition, Metaphor, ๋“ค์—ฌ์“ฐ๊ธฐ ์˜์–ด ๋ถˆ๋ฆฟ ๋“ฑ)
return md.split("\n").filter((line) => {
const t = line.trim();
if (!t) return true;
if (/^[*\-]?\s*\*?(Technical|Simplified|Metaphor|Definition|Note|Step\s*\d|Example|Here'?s|Okay|First,|Now,|Let'?s|I'?ll|I will)/i.test(t)) return false;
if (/^\s{2,}[*\-]/.test(line) && !/[๊ฐ€-ํžฃ]/.test(t)) return false; // ํ•œ๊ธ€ ์—†๋Š” ๋“ค์—ฌ์“ฐ๊ธฐ ์˜์–ด ๋ถˆ๋ฆฟ
return true;
}).join("\n").replace(/\n{3,}/g, "\n\n").trim();
}
function eduRender(md) {
const c = eduCleanup(md);
try { return DOMPurify.sanitize(marked.parse(c)); }
catch { return c.replace(/</g, "&lt;").replace(/\n/g, "<br>"); }
}
async function askTeacher(q, go) {
if (!q || eduBusy) return;
eduBusy = true;
if (eduGoto) eduGoto.hidden = true;
eduMessages.insertAdjacentHTML("beforeend", `<div class="edu-msg edu-user">${q.replace(/</g, "&lt;")}</div>`);
const ai = document.createElement("div");
ai.className = "edu-msg edu-ai";
ai.innerHTML = '<span class="edu-typing">๐Ÿง‘โ€๐Ÿซ ์„ ์ƒ๋‹˜์ด ๋‹ต๋ณ€์„ ์ค€๋น„ํ•˜๊ณ  ์žˆ์–ด์š”โ€ฆ</span>';
eduMessages.appendChild(ai);
eduMessages.scrollTop = eduMessages.scrollHeight;
let acc = "";
try {
const r = await fetch("/api/chat", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: [{ role: "user", content: q + "\n\n(์–ด๋ฅด์‹ ๋„ ์ดํ•ดํ•˜์‹ค ์ˆ˜ ์žˆ๋„๋ก, ์นœ๊ทผํ•œ ์„ ์ƒ๋‹˜์ฒ˜๋Ÿผ ์•„์ฃผ ์‰ฝ๊ฒŒ ํ•œ๊ตญ์–ด๋กœ ํ’€์–ด์„œ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”. ์˜์–ดยท๊ฐœ์š”ยท๋ชฉ์ฐจ ์—†์ด ๋ฐ”๋กœ ์„ค๋ช…๋งŒ.)" }], max_tokens: 1500, temperature: 0.4 }),
});
const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "";
while (true) {
const { done, value } = await reader.read(); if (done) break;
buf += dec.decode(value, { stream: true });
let nl;
while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
if (!line.startsWith("data:")) continue;
const d = line.slice(5).trim(); if (d === "[DONE]" || !d) continue;
try { const j = JSON.parse(d); const c = j.choices?.[0]?.delta?.content || ""; if (c) { acc += c; ai.innerHTML = eduRender(acc); eduMessages.scrollTop = eduMessages.scrollHeight; } } catch {}
}
}
if (!acc) ai.innerHTML = "๋‹ต๋ณ€์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์–ด์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.";
} catch (e) { ai.innerHTML = "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”: " + e.message; }
if (go && GO_LABEL[go] && eduGoto) {
eduGoto.hidden = false;
eduGoto.innerHTML = `<button class="edu-go-btn">โ–ถ ์ง€๊ธˆ ๋ฐ”๋กœ '${GO_LABEL[go]}'์—์„œ ํ•ด๋ณด๊ธฐ</button>`;
eduGoto.firstChild.onclick = () => switchTab(go);
}
eduBusy = false;
}
$$(".edu-card").forEach((c) => { c.onclick = () => askTeacher(c.dataset.q, c.dataset.go || ""); });
function eduAsk() { const q = (eduInput && eduInput.value || "").trim(); if (q) { askTeacher(q, ""); eduInput.value = ""; } }
if (eduSend) eduSend.onclick = eduAsk;
if (eduInput) eduInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); eduAsk(); } });
// ===== ํžˆ์–ด๋กœ ๋“œ๋กญ์กด (๋ฐฉ์•ˆ1) =====
const heroDropzone = $("#hero-dropzone");
if (heroDropzone) heroDropzone.onclick = () => fileInput.click();
// ============================================================
// ===== ๐Ÿ›๏ธ ํ–‰์ • AX ํƒญ (2026-06-10) =====
// ============================================================
// ๋ฐ๋ชจ ํŒŒ์ผ ๋ชฉ๋ก (static/demo/ ์— ์—…๋กœ๋“œ๋œ ํŒŒ์ผ๋“ค)
const DEMO_FILES_EXCEL = [
{name:"01_๊ด‘์ฃผ๋™๊ตฌ_๋ณต์ง€ํ˜„ํ™ฉ.xlsx", label:"๊ด‘์ฃผ ๋™๊ตฌ (XLSX)", ext:"xlsx"},
{name:"02_์ „๋‚จ๋„์ฒญ_๋ณต์ง€์ˆ˜๊ธ‰.xlsx", label:"์ „๋‚จ ๋„์ฒญ (XLSX)", ext:"xlsx"},
{name:"03_์ˆœ์ฒœ์‹œ_๋ณต์ง€ํ†ต๊ณ„.xlsx", label:"์ˆœ์ฒœ์‹œ (XLSX)", ext:"xlsx"},
{name:"04_๋ชฉํฌ์‹œ_๋ณต์ง€๋Œ€์žฅ.xlsx", label:"๋ชฉํฌ์‹œ (XLSX)", ext:"xlsx"},
{name:"05_์—ฌ์ˆ˜์‹œ_๋ณต์ง€ํ˜„ํ™ฉ.xlsx", label:"์—ฌ์ˆ˜์‹œ (XLSX)", ext:"xlsx"},
];
const DEMO_FILES_FORMAT = [
{name:"๋ณต์ง€์‹ ์ฒญ_๊ด‘์ฃผ๋™๊ตฌ.hwpx", label:"๊ด‘์ฃผ๋™๊ตฌ ๊ณต๋ฌธ", ext:"hwpx"},
{name:"๋ณต์ง€์‹ ์ฒญ_์ „๋‚จ๋„์ฒญ.xlsx", label:"์ „๋‚จ๋„์ฒญ Excel", ext:"xlsx"},
{name:"๋ณต์ง€์ˆ˜๊ธ‰ํ˜„ํ™ฉ.csv", label:"์ˆ˜๊ธ‰ํ˜„ํ™ฉ CSV", ext:"csv"},
{name:"์‚ฌ์—…์„ค๋ช…ํšŒ_์ž๋ฃŒ.pptx", label:"์‚ฌ์—…์„ค๋ช… PPT", ext:"pptx"},
{name:"๋ฏผ์›์ ‘์ˆ˜๋Œ€์žฅ.docx", label:"๋ฏผ์› Word", ext:"docx"},
{name:"๊ณต๋ฌธ_๋ณต์ง€๊ณต๋žŒ.pdf", label:"๋ณต์ง€ ๊ณต๋ฌธ PDF", ext:"pdf"},
{name:"๋ณต์ง€์‹ ์ฒญ_ํ•œ๊ธ€.hwpx", label:"ํ•œ๊ธ€ ์‹ ์ฒญ์„œ", ext:"hwpx"},
];
function encodeFilename(s) {
return encodeURIComponent(s);
}
function renderFileChips(containerId, demoFiles) {
const container = $("#" + containerId);
if (!container) return;
container.innerHTML = demoFiles.map(f => `
<div class="ax-file-chip" data-demo="${f.name}" data-label="${f.label}">
<span class="ax-ext-badge ax-ext-${f.ext}">${f.ext.toUpperCase()}</span>
${f.label}
<span style="opacity:0.5;font-size:0.75rem">๐Ÿ‘</span>
</div>
`).join("");
container.querySelectorAll(".ax-file-chip").forEach(chip => {
chip.onclick = () => axPreviewDemo(chip.dataset.demo, chip.dataset.label);
});
}
async function axPreviewDemo(filename, label) {
const modal = $("#ax-modal");
const body = $("#ax-modal-body");
const title = $("#ax-modal-title");
if (!modal || !body) return;
if (title) title.textContent = "\uD83D\uDCC4 " + label + " \u2014 \uBBF8\uB9AC\uBCF4\uAE30";
body.innerHTML = '<div class="ax-loading">\u23F3 \uBD88\uB7EC\uC624\uB294 \uC911...</div>';
modal.hidden = false;
try {
const r = await fetch("/api/demo/preview/" + encodeURIComponent(filename));
if (!r.ok) { body.innerHTML = "<p style='color:#b91c1c'>\uBBF8\uB9AC\uBCF4\uAE30 \uC2E4\uD328 (" + r.status + ")</p>"; return; }
const d = await r.json();
body.innerHTML = (d.rows > 0) ? (d.html || "<p>\uBBF8\uB9AC\uBCF4\uAE30 \uC5C6\uC74C</p>") : "<p style='color:#92400e'>\u26A0\uFE0F \uD30C\uC2F1 \uC2E4\uD328</p>";
} catch(e) {
body.innerHTML = "<p style='color:red'>\uC624\uB958: " + (e && e.message ? e.message : e) + "</p>";
}
}
// ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ
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 = '<div class="ax-loading">โณ ํ†ตํ•ฉ ์ค‘...</div>'; }
try {
const fd = new FormData();
axExcelFiles.forEach(f => fd.append("files", f));
const r = await fetch("/api/excel/merge", {method: "POST", body: fd});
const d = await r.json();
if (axExcelResult) {
const srcHtml = (d.sources || []).map(s => `<span class="ax-source-chip">๐Ÿ“„ ${s.ํŒŒ์ผ} (${s.๊ฑด์ˆ˜}๊ฑด)</span>`).join("");
axExcelResult.innerHTML = `<div class="ax-source-list">${srcHtml}</div>${d.html || ""}`;
}
} catch(e) {
if (axExcelResult) axExcelResult.innerHTML = `<p style="color:red">์˜ค๋ฅ˜: ${e.message}</p>`;
}
axExcelRun.disabled = false;
axExcelRun.innerHTML = "<span>โšก</span> ํ†ตํ•ฉ ์‹คํ–‰";
};
// โ”€โ”€ 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 = '<div class="ax-loading">โณ ํ†ตํ•ฉ ์ค‘...</div>'; }
try {
const fd = new FormData();
axFormatFiles.forEach(f => fd.append("files", f));
const r = await fetch("/api/files/merge-formats", {method: "POST", body: fd});
const d = await r.json();
if (axFormatResult) {
const srcHtml = (d.sources || []).map(s => `<span class="ax-source-chip">${s.ํฌ๋งท} ${s.ํŒŒ์ผ} (${s.๊ฑด์ˆ˜}๊ฑด)</span>`).join("");
axFormatResult.innerHTML = `<div class="ax-source-list">${srcHtml}</div>${d.html || ""}`;
}
} catch(e) {
if (axFormatResult) axFormatResult.innerHTML = `<p style="color:red">์˜ค๋ฅ˜: ${e.message}</p>`;
}
axFormatRun.disabled = false;
axFormatRun.innerHTML = "<span>โšก</span> ํ†ตํ•ฉ ์‹คํ–‰";
};
// ===== 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);