diff --git a/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py b/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py index 26be22a0e..42179624c 100644 --- a/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py +++ b/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py @@ -13,7 +13,7 @@ from common_util.common_util_auth import verify_session router = APIRouter(prefix="/knowledge-chat", tags=["B01_KnowledgeChat"]) KNOWLEDGE = Path(__file__).resolve().parents[1] / "resources" / "knowledge" -EXCLUDED = ("실무문서", "stmate", "다산소프트") +EXCLUDED = ("실무문서", "stmate", "다산소프트", "technical_info") MODEL = "gemini-3.8-flash" diff --git a/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts b/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts index 519ba5bf9..b718490a0 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts @@ -2,6 +2,7 @@ import { API_BASE_URL } from "@config/config_frontend"; type Source = { path: string; excerpt: string }; type ChatAnswer = { answer: string; sources: Source[]; detail?: string }; +const chatIcon = ``; export function buildKnowledgeChat(): HTMLElement { const wrap = document.createElement("div"); @@ -9,25 +10,44 @@ export function buildKnowledgeChat(): HTMLElement { const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "b01-knowledge-chat__toggle"; - toggle.textContent = "원문 안내"; + 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.setAttribute("aria-label", "지식DB 원문 안내 채팅"); + panel.id = "b01-knowledge-chat-panel"; + panel.setAttribute("aria-label", "챗봇"); + toggle.setAttribute("aria-controls", panel.id); const header = document.createElement("header"); - header.textContent = "지식DB 원문 안내"; + 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.placeholder = "메시지를 입력하세요"; input.setAttribute("aria-label", "질문"); const submit = document.createElement("button"); submit.type = "submit"; @@ -36,11 +56,15 @@ export function buildKnowledgeChat(): HTMLElement { panel.append(header, messages, form); wrap.append(toggle, panel); - toggle.addEventListener("click", () => { - panel.hidden = !panel.hidden; + const setOpen = (open: boolean): void => { + panel.hidden = !open; toggle.setAttribute("aria-expanded", String(!panel.hidden)); - if (!panel.hidden) input.focus(); - }); + 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(); diff --git a/B01_Dashboard/B01_Dashboard_UI_Style.css b/B01_Dashboard/B01_Dashboard_UI_Style.css index 93d70fa3c..16efe22a4 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Style.css +++ b/B01_Dashboard/B01_Dashboard_UI_Style.css @@ -535,16 +535,30 @@ .b01-knowledge-chat__toggle, .b01-knowledge-chat__form button { - padding: 10px 16px; border: 0; - border-radius: 8px; background: var(--color-royal-amethyst, #6843a7); color: #fff; font: inherit; cursor: pointer; } +.b01-knowledge-chat__toggle { + display: grid; + width: 58px; + height: 58px; + place-items: center; + border-radius: 50%; + box-shadow: 0 4px 16px #0004; +} + +.b01-knowledge-chat__toggle svg { + width: 30px; + height: 30px; +} + .b01-knowledge-chat__panel { + display: flex; + flex-direction: column; width: min(380px, calc(100vw - 32px)); height: min(540px, calc(100vh - 104px)); border: 1px solid var(--color-border); @@ -560,34 +574,76 @@ } .b01-knowledge-chat__panel header { - padding: 12px 16px; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; border-bottom: 1px solid var(--color-border); - font-weight: bold; +} + +.b01-knowledge-chat__avatar { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border-radius: 50%; + background: var(--color-royal-amethyst, #6843a7); + color: #fff; +} + +.b01-knowledge-chat__avatar svg { + width: 21px; + height: 21px; +} + +.b01-knowledge-chat__close { + margin-left: auto; + border: 0; + background: transparent; + color: inherit; + font: inherit; + font-size: 26px; + cursor: pointer; } .b01-knowledge-chat__messages { - height: calc(100% - 106px); - padding: 12px; + display: flex; + flex: 1; + flex-direction: column; + gap: 12px; + min-height: 0; + padding: 16px 12px; overflow-y: auto; } .b01-knowledge-chat__user, .b01-knowledge-chat__reply { - margin: 0 0 12px; - padding: 10px; - border-radius: 8px; + max-width: 88%; + margin: 0; + padding: 10px 12px; + border-radius: 14px; + overflow-wrap: anywhere; white-space: pre-wrap; } .b01-knowledge-chat__user { + align-self: flex-end; + border-bottom-right-radius: 4px; background: var(--color-royal-amethyst, #6843a7); color: #fff; } .b01-knowledge-chat__reply { + align-self: flex-start; + border-bottom-left-radius: 4px; background: var(--color-background, #f4f4f4); } +.b01-knowledge-chat__reply details { + margin-top: 8px; + font-size: 0.9em; +} + .b01-knowledge-chat__reply summary { cursor: pointer; overflow-wrap: anywhere; @@ -602,6 +658,7 @@ .b01-knowledge-chat__form { display: flex; + align-items: center; gap: 8px; padding: 8px; border-top: 1px solid var(--color-border); @@ -615,3 +672,14 @@ border-radius: 6px; font: inherit; } + +.b01-knowledge-chat__form button { + padding: 9px 14px; + border-radius: 8px; +} + +.b01-knowledge-chat button:focus-visible, +.b01-knowledge-chat input:focus-visible { + outline: 2px solid var(--color-royal-amethyst, #6843a7); + outline-offset: 2px; +} diff --git a/resources/tester/test_b01_knowledge_chat.py b/resources/tester/test_b01_knowledge_chat.py index 237e79bb1..bf7667c07 100644 --- a/resources/tester/test_b01_knowledge_chat.py +++ b/resources/tester/test_b01_knowledge_chat.py @@ -49,9 +49,10 @@ def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatc result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="임도 기준"), {})) assert result["answer"] == "설명 [1]" paths = {source["path"] for source in result["sources"]} - assert paths == {"original/법률/현행.md", "technical_info/_검토.md"} + assert paths == {"original/법률/현행.md"} assert "실무문서" not in sent["prompt"] assert "비공개 사례" not in sent["prompt"] assert "STmate" not in sent["prompt"] assert "다산소프트" not in sent["prompt"] + assert "기술정보 안내" not in sent["prompt"] chat._sections.cache_clear()