120 lines
5.1 KiB
TypeScript
120 lines
5.1 KiB
TypeScript
import { API_BASE_URL } from "@config/config_frontend";
|
||
|
||
type Source = { path: string; excerpt: string };
|
||
type ChatAnswer = { answer: string; sources: Source[]; detail?: string };
|
||
const chatIcon = `<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 5h16a5 5 0 0 1 5 5v10a5 5 0 0 1-5 5H13l-7 4v-5a5 5 0 0 1-3-4V10a5 5 0 0 1 5-5Z"/><path d="M11 19c0-5 4-8 10-8 0 6-3 10-8 10"/><path d="M11 22c2-3 5-5 9-7"/></svg>`;
|
||
|
||
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", "챗봇");
|
||
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");
|
||
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("input");
|
||
input.type = "text";
|
||
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 }),
|
||
});
|
||
const result = (await response.json()) as ChatAnswer;
|
||
if (!response.ok) throw new Error(result.detail || "답변을 받지 못했습니다.");
|
||
reply.textContent = result.answer;
|
||
if (result.sources.length) {
|
||
const sources = document.createElement("details");
|
||
const summary = document.createElement("summary");
|
||
summary.textContent = `근거 원문 ${result.sources.length}건`;
|
||
sources.append(summary);
|
||
result.sources.forEach((source, index) => {
|
||
const item = document.createElement("details");
|
||
const title = document.createElement("summary");
|
||
title.textContent = `[${index + 1}] ${source.path}`;
|
||
const excerpt = document.createElement("pre");
|
||
excerpt.textContent = source.excerpt;
|
||
item.append(title, excerpt);
|
||
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;
|
||
}
|