import { API_BASE_URL } from "@config/config_frontend";
import { renderHelpMarkdown } from "@ui/ui_template_help";
import { openModal } from "@ui/ui_template_modal";
type Source = { path: string; excerpt: string };
type ChatAnswer = { answer: string; sources: Source[]; detail?: string };
type ChatTurn = { role: "user" | "assistant"; content: string };
const chatIcon = ``;
export function buildKnowledgeChat(): HTMLElement {
const wrap = document.createElement("div");
wrap.className = "b01-knowledge-chat";
const toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "b01-knowledge-chat__toggle";
toggle.innerHTML = chatIcon;
toggle.setAttribute("aria-label", "챗봇 열기");
toggle.title = "챗봇";
toggle.setAttribute("aria-expanded", "false");
const panel = document.createElement("section");
panel.className = "b01-knowledge-chat__panel";
panel.hidden = true;
panel.id = "b01-knowledge-chat-panel";
panel.setAttribute("aria-label", "챗봇");
panel.addEventListener(
"wheel",
(event) => {
if (!messages.contains(event.target as Node)) event.preventDefault();
},
{ passive: false },
);
toggle.setAttribute("aria-controls", panel.id);
const header = document.createElement("header");
const avatar = document.createElement("span");
avatar.className = "b01-knowledge-chat__avatar";
avatar.innerHTML = chatIcon;
const title = document.createElement("strong");
title.textContent = "챗봇";
const close = document.createElement("button");
close.type = "button";
close.className = "b01-knowledge-chat__close";
close.textContent = "×";
close.setAttribute("aria-label", "챗봇 닫기");
header.append(avatar, title, close);
const messages = document.createElement("div");
let history: ChatTurn[] = [];
messages.className = "b01-knowledge-chat__messages";
messages.setAttribute("role", "log");
messages.setAttribute("aria-live", "polite");
const welcome = document.createElement("p");
welcome.className = "b01-knowledge-chat__reply";
welcome.textContent =
"안녕하세요! 법령이나 지침에 대해 질문해 주세요. 근거 문서도 함께 보여드릴게요.";
messages.append(welcome);
const form = document.createElement("form");
form.className = "b01-knowledge-chat__form";
const input = document.createElement("textarea");
input.rows = 2;
input.required = true;
input.maxLength = 500;
input.placeholder = "메시지를 입력하세요";
input.setAttribute("aria-label", "질문");
const submit = document.createElement("button");
submit.type = "submit";
submit.textContent = "보내기";
form.append(input, submit);
panel.append(header, messages, form);
wrap.append(toggle, panel);
const setOpen = (open: boolean): void => {
panel.hidden = !open;
toggle.setAttribute("aria-expanded", String(!panel.hidden));
toggle.setAttribute("aria-label", open ? "챗봇 닫기" : "챗봇 열기");
if (open) input.focus();
else toggle.focus();
};
toggle.addEventListener("click", () => setOpen(Boolean(panel.hidden)));
close.addEventListener("click", () => setOpen(false));
form.addEventListener("submit", async (event) => {
event.preventDefault();
const question = input.value.trim();
if (question.length < 2) return;
const userLine = document.createElement("p");
userLine.className = "b01-knowledge-chat__user";
userLine.textContent = question;
messages.append(userLine);
input.value = "";
input.disabled = submit.disabled = true;
const reply = document.createElement("div");
reply.className = "b01-knowledge-chat__reply";
reply.textContent = "원문을 찾는 중입니다…";
messages.append(reply);
messages.scrollTop = messages.scrollHeight;
try {
const response = await fetch(`${API_BASE_URL}/dashboard/knowledge-chat`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question, history }),
});
const result = (await response.json()) as ChatAnswer;
if (!response.ok) throw new Error(result.detail || "답변을 받지 못했습니다.");
reply.textContent = result.answer;
history = [
...history,
{ role: "user", content: question },
{ role: "assistant", content: result.answer },
].slice(-6) as ChatTurn[];
if (result.sources.length) {
const sources = document.createElement("details");
sources.className = "b01-knowledge-chat__sources";
const summary = document.createElement("summary");
summary.textContent = `근거 원문 ${result.sources.length}건`;
sources.append(summary);
result.sources.forEach((source, index) => {
const item = document.createElement("button");
item.type = "button";
item.className = "b01-knowledge-chat__source";
const filename = source.path.split("/").pop()?.replace(/\.md$/i, "") ?? source.path;
const name = document.createElement("strong");
name.textContent = `[${index + 1}] ${filename}`;
const location = document.createElement("span");
location.textContent = `원문 · ${source.path.replace(/^original\//, "")}`;
item.append(name, location);
item.addEventListener("click", () => void openSource(source.path, filename));
sources.append(item);
});
reply.append(sources);
}
} catch (error) {
reply.textContent = error instanceof Error ? error.message : "답변을 받지 못했습니다.";
} finally {
input.disabled = submit.disabled = false;
input.focus();
messages.scrollTop = messages.scrollHeight;
}
});
return wrap;
}
async function openSource(path: string, title: string): Promise {
openModal({
title,
closeLabel: "닫기",
dialogClass: "b01-knowledge-source-modal",
mount: (body) => {
const article = document.createElement("article");
article.className = "b01-knowledge-source-modal__content";
article.textContent = "원문을 불러오는 중입니다…";
body.append(article);
void fetch(`${API_BASE_URL}/dashboard/knowledge-chat/source`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path }),
})
.then(async (response) => {
const result = (await response.json()) as { content?: string; detail?: string };
if (!response.ok) throw new Error(result.detail || "원문을 불러오지 못했습니다.");
article.innerHTML = renderHelpMarkdown(result.content ?? "");
})
.catch((error: unknown) => {
article.textContent =
error instanceof Error ? error.message : "원문을 불러오지 못했습니다.";
});
},
});
}