Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import os | |
| from threading import Lock | |
| import gradio as gr | |
| from megumin_agent.chat import ChatServices | |
| from megumin_agent.chat import create_chat_services | |
| from megumin_agent.chat import stream_chat | |
| INITIAL_GREETING = "๋ด ์ด๋ฆ์ ๋ฉ๊ตฌ๋ฐ! ํ๋ง์กฑ ์ ์ผ์ ๋ง๋ฒ์ฌ์ด์, ํญ๋ ฌ ๋ง๋ฒ์ ํผ์น๋ ์!" | |
| INITIAL_HISTORY = [{"role": "assistant", "content": INITIAL_GREETING}] | |
| SERVICES: ChatServices | None = None | |
| SERVICES_LOCK = Lock() | |
| MEGUMIN_IMAGE_URL = os.getenv( | |
| "MEGUMIN_IMAGE_URL", | |
| "https://huggingface.co/datasets/Junhoee/megumin-chat/resolve/main/%EB%A9%94%EA%B5%AC%EB%B0%8D%EC%82%AC%EC%A7%84.png", | |
| ) | |
| MEGUMIN_SVG = """ | |
| <svg viewBox="0 0 420 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Megumin inspired illustration"> | |
| <defs> | |
| <linearGradient id="cape" x1="0" y1="0" x2="1" y2="1"> | |
| <stop offset="0%" stop-color="#7b0f1b"/> | |
| <stop offset="100%" stop-color="#2c0610"/> | |
| </linearGradient> | |
| <linearGradient id="hat" x1="0" y1="0" x2="1" y2="1"> | |
| <stop offset="0%" stop-color="#41221f"/> | |
| <stop offset="100%" stop-color="#120b12"/> | |
| </linearGradient> | |
| <linearGradient id="gold" x1="0" y1="0" x2="1" y2="1"> | |
| <stop offset="0%" stop-color="#ffd86a"/> | |
| <stop offset="100%" stop-color="#e59a22"/> | |
| </linearGradient> | |
| </defs> | |
| <ellipse cx="212" cy="520" rx="120" ry="22" fill="rgba(20,6,10,0.28)"/> | |
| <circle cx="300" cy="104" r="24" fill="rgba(255,114,61,0.28)"/> | |
| <circle cx="334" cy="138" r="12" fill="rgba(255,201,115,0.32)"/> | |
| <path d="M132 180 C116 258 106 344 114 454 L306 454 C314 344 304 258 288 180 Z" fill="url(#cape)"/> | |
| <path d="M174 114 C136 136 120 164 124 206 C132 292 288 292 296 206 C300 164 284 136 246 114 Z" fill="#f6d4bf"/> | |
| <path d="M152 114 C164 74 256 72 274 116 C262 102 240 94 210 94 C182 94 162 100 152 114 Z" fill="#5a2f1f"/> | |
| <path d="M150 110 C120 124 92 170 104 198 C110 212 128 214 146 208 L140 168 C154 156 192 150 228 150 C266 150 298 158 312 170 L308 206 C326 212 344 210 350 198 C362 170 334 124 304 110 C284 88 250 76 210 76 C172 76 140 88 150 110 Z" fill="url(#hat)"/> | |
| <path d="M120 138 L84 154 L108 178 L142 166 Z" fill="#d33b2d"/> | |
| <path d="M310 142 L344 158 L320 180 L286 168 Z" fill="#d33b2d"/> | |
| <path d="M170 274 C194 288 226 290 250 274" stroke="#6a2d1b" stroke-width="8" stroke-linecap="round" fill="none"/> | |
| <circle cx="180" cy="210" r="11" fill="#4a1d14"/> | |
| <circle cx="242" cy="210" r="11" fill="#4a1d14"/> | |
| <path d="M132 108 L170 70 L206 116 Z" fill="url(#gold)"/> | |
| <circle cx="164" cy="92" r="16" fill="#c71f2d"/> | |
| <path d="M246 78 L340 28 L350 54 L264 96 Z" fill="#5a2f1f"/> | |
| <circle cx="338" cy="32" r="16" fill="url(#gold)"/> | |
| <path d="M292 186 L372 78 L388 92 L318 196 Z" fill="#5a2f1f"/> | |
| <path d="M372 74 L388 48 L402 86 Z" fill="#ff8b2f"/> | |
| <path d="M318 196 L364 250" stroke="#5a2f1f" stroke-width="12" stroke-linecap="round"/> | |
| <path d="M340 238 C376 214 410 238 398 276 C386 316 330 318 316 274 C310 256 320 246 340 238 Z" fill="#ff7437" opacity="0.9"/> | |
| <path d="M160 456 L188 300 L232 300 L260 456 Z" fill="#23131a"/> | |
| <path d="M154 302 L114 454" stroke="#120b12" stroke-width="18" stroke-linecap="round"/> | |
| <path d="M266 302 L306 454" stroke="#120b12" stroke-width="18" stroke-linecap="round"/> | |
| </svg> | |
| """.strip() | |
| CUSTOM_CSS = """ | |
| :root { | |
| --megumin-panel: rgba(255, 244, 232, 0.12); | |
| --megumin-panel-strong: rgba(255, 244, 232, 0.18); | |
| --megumin-line: rgba(255, 216, 169, 0.22); | |
| --megumin-text: #fff8ef; | |
| --megumin-shadow: 0 24px 80px rgba(7, 1, 4, 0.42); | |
| } | |
| body, .gradio-container { | |
| background: | |
| radial-gradient(circle at 14% 18%, rgba(255, 145, 71, 0.16), transparent 26%), | |
| radial-gradient(circle at 82% 14%, rgba(222, 49, 81, 0.18), transparent 24%), | |
| linear-gradient(135deg, #1d0710 0%, #12050b 48%, #26141f 100%); | |
| color: var(--megumin-text); | |
| } | |
| .gradio-container { | |
| max-width: 1900px !important; | |
| padding: 24px 28px 28px !important; | |
| } | |
| .megumin-stage { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 0; | |
| overflow: hidden; | |
| pointer-events: none; | |
| } | |
| .megumin-stage::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(90deg, rgba(18, 5, 11, 0.82) 0%, rgba(18, 5, 11, 0.35) 42%, rgba(18, 5, 11, 0.72) 100%); | |
| } | |
| .stage-portrait { | |
| position: absolute; | |
| right: 3%; | |
| bottom: -1.5rem; | |
| width: min(40vw, 560px); | |
| opacity: 0.22; | |
| filter: drop-shadow(0 30px 80px rgba(0, 0, 0, 0.35)); | |
| } | |
| .stage-burst { | |
| position: absolute; | |
| right: 19%; | |
| top: 10%; | |
| width: 26rem; | |
| height: 26rem; | |
| border-radius: 999px; | |
| background: radial-gradient(circle, rgba(255, 175, 69, 0.35), rgba(255, 119, 53, 0.14), transparent 68%); | |
| filter: blur(14px); | |
| } | |
| .shell { | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .main-layout { | |
| display: grid; | |
| grid-template-columns: 1fr 1.6fr 1fr; | |
| gap: 20px; | |
| align-items: stretch; | |
| } | |
| .left-panel { | |
| grid-column: 1; | |
| } | |
| .chat-panel-col { | |
| grid-column: 2; | |
| } | |
| .right-panel { | |
| grid-column: 3; | |
| } | |
| .glass-panel { | |
| background: linear-gradient(180deg, var(--megumin-panel-strong) 0%, var(--megumin-panel) 100%); | |
| border: 1px solid var(--megumin-line); | |
| box-shadow: var(--megumin-shadow); | |
| backdrop-filter: blur(14px); | |
| border-radius: 28px; | |
| overflow: hidden; | |
| } | |
| .profile-panel, | |
| .visual-panel, | |
| .chat-panel { | |
| min-height: 620px; | |
| } | |
| .panel-head { | |
| padding: 24px 24px 10px; | |
| } | |
| .eyebrow { | |
| font-size: 0.82rem; | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| color: rgba(255, 217, 174, 0.82); | |
| margin-bottom: 10px; | |
| } | |
| .panel-title { | |
| font-size: 1.95rem; | |
| line-height: 1.1; | |
| font-weight: 800; | |
| margin: 0; | |
| color: #ffffff; | |
| } | |
| .profile-panel .eyebrow, | |
| .visual-panel .eyebrow, | |
| .profile-panel .panel-title, | |
| .visual-panel .panel-title { | |
| color: #ffffff !important; | |
| } | |
| .panel-copy, | |
| .box-title, | |
| .example-box li, | |
| .visual-note p, | |
| .quote, | |
| .quote small { | |
| color: #ffffff !important; | |
| } | |
| .panel-copy { | |
| margin: 12px 0 0; | |
| font-size: 0.98rem; | |
| line-height: 1.75; | |
| } | |
| .example-box, | |
| .visual-note { | |
| margin: 18px 24px 0; | |
| padding: 16px 18px 14px; | |
| border-radius: 22px; | |
| background: rgba(255, 248, 239, 0.08); | |
| border: 1px solid rgba(255, 222, 189, 0.12); | |
| } | |
| .box-title { | |
| margin: 0 0 10px; | |
| font-size: 0.95rem; | |
| font-weight: 700; | |
| } | |
| .example-box ul { | |
| margin: 0; | |
| padding-left: 1.1rem; | |
| line-height: 1.7; | |
| } | |
| .example-box li { | |
| margin: 0 0 0.45rem; | |
| } | |
| .chat-hero { | |
| display: none; | |
| } | |
| .chat-header { | |
| padding: 26px 28px 10px; | |
| display: flex; | |
| align-items: end; | |
| justify-content: space-between; | |
| gap: 16px; | |
| } | |
| .chat-header h1 { | |
| margin: 0; | |
| font-size: 2rem; | |
| font-weight: 800; | |
| color: #ffffff; | |
| } | |
| .chat-header .eyebrow, | |
| .chat-header p, | |
| .status-pill { | |
| color: #111111 !important; | |
| } | |
| .status-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 14px; | |
| border-radius: 999px; | |
| background: rgba(255, 209, 143, 0.12); | |
| font-size: 0.85rem; | |
| border: 1px solid rgba(255, 213, 157, 0.18); | |
| } | |
| .status-dot { | |
| width: 9px; | |
| height: 9px; | |
| border-radius: 999px; | |
| background: #ffb04a; | |
| box-shadow: 0 0 18px rgba(255, 176, 74, 0.9); | |
| } | |
| .mood-badge-slot { | |
| margin: 0 18px 10px; | |
| } | |
| .mood-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 10px; | |
| border-radius: 999px; | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| background: rgba(255, 248, 239, 0.12); | |
| border: 1px solid rgba(255, 226, 186, 0.16); | |
| color: #fff7eb; | |
| } | |
| .mood-badge::before { | |
| content: ""; | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 999px; | |
| background: currentColor; | |
| box-shadow: 0 0 10px currentColor; | |
| } | |
| .mood-calm { color: #ffe1b2; } | |
| .mood-angry { color: #ff8b6e; } | |
| .mood-explosion { color: #ffbe45; } | |
| .mood-proud { color: #ffd86a; } | |
| .mood-meal { color: #ffd2a8; } | |
| .runtime-status { | |
| margin: 0 18px 12px; | |
| padding: 10px 14px; | |
| border-radius: 16px; | |
| background: rgba(255, 234, 201, 0.08); | |
| border: 1px solid rgba(255, 213, 157, 0.12); | |
| color: #fff4dc; | |
| font-size: 0.92rem; | |
| } | |
| .chatbot-wrap { | |
| padding: 0 18px 12px; | |
| } | |
| .chatbot-wrap .message, | |
| .chatbot-wrap .bubble { | |
| backdrop-filter: blur(10px); | |
| } | |
| .chatbot-wrap .message.user, | |
| .chatbot-wrap .message.user .bubble, | |
| .chatbot-wrap .message-row.user .bubble { | |
| background: rgba(255, 239, 219, 0.92) !important; | |
| color: #3c1a16 !important; | |
| } | |
| .chatbot-wrap .message.bot, | |
| .chatbot-wrap .message.bot .bubble, | |
| .chatbot-wrap .message-row.bot .bubble, | |
| .chatbot-wrap .message-row.assistant .bubble { | |
| background: var(--color-background-secondary, #f4f4f5) !important; | |
| color: var(--body-text-color, #1f2937) !important; | |
| border: 1px solid var(--border-color-primary, rgba(0, 0, 0, 0.08)) !important; | |
| } | |
| .chat-history { | |
| height: 470px; | |
| } | |
| .input-zone { | |
| padding: 0 18px 18px; | |
| } | |
| .input-row { | |
| display: flex; | |
| gap: 12px; | |
| align-items: stretch; | |
| } | |
| .input-message { | |
| flex: 8 1 0; | |
| min-width: 0; | |
| } | |
| .input-actions { | |
| flex: 2 1 0; | |
| min-width: 110px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .input-zone textarea, | |
| .input-zone input { | |
| color: #111111 !important; | |
| } | |
| .input-zone .gr-textbox, | |
| .input-zone .gr-button, | |
| .input-zone .gr-form { | |
| background: rgba(255, 248, 239, 0.08) !important; | |
| border-color: rgba(255, 226, 186, 0.16) !important; | |
| } | |
| .input-actions .gr-button { | |
| width: 100%; | |
| min-height: 48px; | |
| } | |
| .input-zone .gr-button-primary { | |
| background: linear-gradient(135deg, #f19f35 0%, #d84f34 100%) !important; | |
| color: #fff8ef !important; | |
| border: none !important; | |
| } | |
| .input-zone .gr-button-secondary { | |
| color: #fff8ef !important; | |
| } | |
| .visual-frame { | |
| padding: 24px 22px 16px; | |
| min-height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| } | |
| .archive-image-wrap { | |
| width: 100%; | |
| aspect-ratio: 16 / 9; | |
| overflow: hidden; | |
| border-radius: 20px; | |
| } | |
| .portrait-image, | |
| .portrait-fallback { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| .portrait-image { | |
| object-fit: cover; | |
| box-shadow: 0 22px 40px rgba(0, 0, 0, 0.24); | |
| } | |
| .portrait-fallback { | |
| display: none; | |
| } | |
| .mobile-hero-image { | |
| margin-top: 14px; | |
| width: 100%; | |
| aspect-ratio: 16 / 9; | |
| border-radius: 18px; | |
| overflow: hidden; | |
| } | |
| .mobile-hero-image img, | |
| .mobile-hero-image .portrait-fallback { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| object-fit: cover; | |
| } | |
| @media (max-width: 768px) { | |
| .gradio-container { | |
| padding: 14px 12px 18px !important; | |
| } | |
| .main-layout { | |
| grid-template-columns: 1fr; | |
| gap: 14px; | |
| } | |
| .left-panel, | |
| .right-panel { | |
| display: none !important; | |
| visibility: hidden !important; | |
| width: 0 !important; | |
| min-width: 0 !important; | |
| max-width: 0 !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| overflow: hidden !important; | |
| } | |
| .left-panel .glass-panel, | |
| .right-panel .glass-panel { | |
| display: none !important; | |
| } | |
| .chat-panel-col { | |
| grid-column: 1; | |
| } | |
| .chat-panel { | |
| min-height: auto; | |
| border-radius: 22px; | |
| } | |
| .chat-hero { | |
| display: block; | |
| margin-bottom: 14px; | |
| padding: 18px 18px 14px; | |
| } | |
| .chat-hero .eyebrow { | |
| color: #ffffff !important; | |
| font-size: 0.72rem; | |
| } | |
| .chat-hero h1 { | |
| margin: 0; | |
| font-size: 1.42rem; | |
| line-height: 1.15; | |
| color: #ffffff; | |
| } | |
| .chat-hero p { | |
| margin: 10px 0 0; | |
| color: #ffffff; | |
| line-height: 1.5; | |
| font-size: 0.88rem; | |
| } | |
| .chat-header { | |
| padding: 14px 14px 6px; | |
| } | |
| .chat-header .eyebrow, | |
| .chat-header p, | |
| .status-pill { | |
| color: #ffffff !important; | |
| } | |
| .chat-header h1 { | |
| font-size: 1.28rem; | |
| } | |
| .status-pill { | |
| font-size: 0.72rem; | |
| padding: 7px 10px; | |
| } | |
| .mood-badge-slot { | |
| margin: 0 12px 8px; | |
| } | |
| .mood-badge { | |
| font-size: 0.72rem; | |
| padding: 5px 8px; | |
| } | |
| .chat-history { | |
| height: 34vh !important; | |
| min-height: 220px; | |
| } | |
| .runtime-status { | |
| margin: 0 12px 10px; | |
| font-size: 0.84rem; | |
| } | |
| .input-zone { | |
| padding: 0 12px 12px; | |
| } | |
| .input-row { | |
| gap: 6px; | |
| } | |
| .input-actions .gr-button { | |
| min-height: 40px; | |
| } | |
| } | |
| """.strip() | |
| PROFILE_HTML = """ | |
| <div class="glass-panel profile-panel"> | |
| <div class="panel-head"> | |
| <div class="eyebrow">Character Guide</div> | |
| <h2 class="panel-title">๋ฉ๊ตฌ๋ฐ ํ๋กํ</h2> | |
| <p class="panel-copy"> | |
| ์ด ๋ฉ์ง ์ธ๊ณ์ ์ถ๋ณต์!์ ๋ฑ์ฅ์ธ๋ฌผ๋ก, ํญ๋ ฌ๋ง๋ฒ๋ง์ ๊ณ ์งํ๋ ํ๋ง์กฑ ์ํฌ ์์ ๋์ ๋๋ค. | |
| ์์กด์ฌ ๊ฐํ๊ณ ํ์ธ ๋์น๋ ๋งํฌ๋ฅผ ์ฆ๊ธฐ์ง๋ง, ๋๋ฃ๋ค์๊ฒ๋ ์์ธ๋ก ์ง์ฌ์ ๋ณด์ด๋ ๋ฉด๋ ์์ต๋๋ค. | |
| ์ด ์ฑ๋ด์ ๋ฉ๊ตฌ๋ฐ์ ๋งํฌ์ ๊ฐ์ ์ ์ ์ ์งํ๋ฉด์ ์์ ์ค์ ๊น์ง ํจ๊ป ์ฐธ๊ณ ํด ๋ตํฉ๋๋ค. | |
| </p> | |
| </div> | |
| <div class="example-box"> | |
| <p class="box-title">์ด๋ ๊ฒ ์์ํด๋ณด์ธ์</p> | |
| <ul> | |
| <li>๋ฉ๊ตฌ๋ฐ, ์นด์ฆ๋ง ์จ๋ฅผ ์ด๋ป๊ฒ ์๊ฐํ์ญ๋๊น?</li> | |
| <li>ํญ๋ ฌ๋ง๋ฒ์ ๊ฐ๋ฅด์ณ ์ค ์ฌ๋์ ๋๊ตฌ์ธ๊ฐ์?</li> | |
| <li>์์ฟ ์์ ์ธ์ดํฌ๋ฆฌ๋ ๋ธ๋ ์ดํฌ ์คํ ์ด ๋ฌด์์ธ์ง ์ค๋ช ํด ์ฃผ์ธ์.</li> | |
| <li>ํ๋ง์กฑ๋ค์ด ์๊ธฐ์๊ฐ๋ฅผ ํ ๋ฒ ๋ค๋ ค์ฃผ์๊ฒ ์ต๋๊น?</li> | |
| <li>๋ฉ๊ตฌ๋ฐ์ด๋ผ๋ ์ด๋ฆ์ด ์๊ธฐ๋ค๊ณ ํ๋ฉด ์ด๋ป๊ฒ ๋ฐ์์น์๊ฒ ์ต๋๊น?</li> | |
| </ul> | |
| </div> | |
| </div> | |
| """.strip() | |
| VISUAL_HTML = f""" | |
| <div class="glass-panel visual-panel"> | |
| <div class="visual-frame"> | |
| <div> | |
| <div class="eyebrow">Explosion Archive</div> | |
| <div class="visual-card archive-image-wrap"> | |
| <img | |
| class="portrait-image" | |
| src="{MEGUMIN_IMAGE_URL}" | |
| alt="Megumin" | |
| onerror="this.style.display='none'; this.nextElementSibling.style.display='block';" | |
| /> | |
| <div class="portrait-fallback">{MEGUMIN_SVG}</div> | |
| </div> | |
| <div class="visual-note"> | |
| <p class="box-title">์ด ํ์ด์ง์ ๋ถ์๊ธฐ</p> | |
| <p class="panel-copy"> | |
| ์ด๋์ด ์ ๋ ๋น๊ณผ ํญ๋ ฌ๋ง๋ฒ์ ์๊ด์ด ๊ฐ๋๋ ๋ฌด๋๋ก ๊ตฌ์ฑํ์ต๋๋ค. | |
| ์ค์์ ๋ฐํฌ๋ช ์ฑํ ํจ๋์ ์ค์ ๋ก ๋ฉ๊ตฌ๋ฐ๊ณผ ๋ง์ฃผ ์์ ์ด์ผ๊ธฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋ ๋๋์ ์ฃผ๋๋ก ์ค๊ณํ์ต๋๋ค. | |
| </p> | |
| </div> | |
| </div> | |
| <div class="visual-note"> | |
| <div class="quote"> | |
| "ํญ๋ ฌ๋ง๋ฒ์ด์ผ๋ง๋ก ๊ถ๊ทน์ ์์ . ๊ทธ๋ฆฌ๊ณ ์ด ๋ํ๋, ๊ทธ ์ฅ์ํ ์๋ง์ ๋๋ค." | |
| <small>Megumin Persona Session</small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """.strip() | |
| MOBILE_HERO_HTML = f""" | |
| <div class="glass-panel chat-hero"> | |
| <div class="eyebrow">Megumin Chat</div> | |
| <h1>๋ฉ๊ตฌ๋ฐ๊ณผ ์์ ๋กญ๊ฒ ๋ํํด๋ณด์ธ์!</h1> | |
| <p>ํญ๋ ฌ๋ง๋ฒ์ฌ ๋ฉ๊ตฌ๋ฐ์ ๋งํฌ์ ์์ ์ค์ ์ ํจ๊ป ์ฐธ๊ณ ํ๋ ๋ํํ ์ฑ๋ด์ ๋๋ค.</p> | |
| <div class="mobile-hero-image"> | |
| <img | |
| src="{MEGUMIN_IMAGE_URL}" | |
| alt="Megumin" | |
| onerror="this.style.display='none'; this.nextElementSibling.style.display='block';" | |
| /> | |
| <div class="portrait-fallback">{MEGUMIN_SVG}</div> | |
| </div> | |
| </div> | |
| """.strip() | |
| BACKGROUND_HTML = f""" | |
| <div class="megumin-stage"> | |
| <div class="stage-burst"></div> | |
| <div class="stage-portrait">{MEGUMIN_SVG}</div> | |
| </div> | |
| """.strip() | |
| def get_services() -> ChatServices: | |
| global SERVICES | |
| if SERVICES is None: | |
| with SERVICES_LOCK: | |
| if SERVICES is None: | |
| SERVICES = create_chat_services() | |
| return SERVICES | |
| def classify_mood(user_text: str, assistant_text: str = "") -> tuple[str, str]: | |
| combined = f"{user_text}\n{assistant_text}".lower() | |
| if any(token in combined for token in ["๋ฐ๋ณด", "๋ฉ์ฒญ", "์๊ธฐ", "์ธ๋ชจ์", "๋ฅ์ณ", "ํ์ฌ"]): | |
| return "๋ฐ๋", "mood-angry" | |
| if any(token in combined for token in ["ํญ๋ ฌ", "์ต์คํ๋ก์ ", "์ด", "๋ง๋ฒ"]): | |
| return "ํญ๋ ฌ ์ค๋น", "mood-explosion" | |
| if any(token in combined for token in ["๋ฐฅ", "๋จน", "์์ฌ", "๋ฐฐ๊ณ "]): | |
| return "๋ฐฐ๊ณ ํ", "mood-meal" | |
| if any(token in combined for token in ["์ต๊ณ ", "๋๋จ", "๋ฉ์ง", "์นญ์ฐฌ", "ํ๋ง์กฑ"]): | |
| return "์๊ธฐ์์", "mood-proud" | |
| return "ํ์จ", "mood-calm" | |
| def render_mood_badge(label: str, mood_class: str) -> str: | |
| return f'<div class="mood-badge {mood_class}">์ํ: {label}</div>' | |
| def current_badge(user_text: str = "", assistant_text: str = "") -> str: | |
| label, mood_class = classify_mood(user_text, assistant_text) | |
| return render_mood_badge(label, mood_class) | |
| def initial_history() -> list[dict[str, str]]: | |
| return [dict(item) for item in INITIAL_HISTORY] | |
| def begin_request( | |
| message: str, | |
| history: list[dict[str, str]], | |
| session_id: str | None, | |
| ): | |
| if not message.strip(): | |
| return history, session_id, "", "", current_badge() | |
| status_text = "์๋น์ค ์ค๋น ์ค..." if SERVICES is None else "๋ต๋ณ ์์ฑ ์ค..." | |
| return history, session_id, message, status_text, current_badge(message) | |
| async def respond( | |
| message: str, | |
| history: list[dict[str, str]], | |
| session_id: str | None, | |
| ): | |
| if not message.strip(): | |
| yield history, session_id, "", "", current_badge() | |
| return | |
| updated_history = list(history) | |
| updated_history.append({"role": "user", "content": message}) | |
| updated_history.append({"role": "assistant", "content": ""}) | |
| yield updated_history, session_id, "", "๋ต๋ณ ์์ฑ ์ค...", current_badge(message) | |
| active_session_id = session_id | |
| got_reply = False | |
| async for partial_text, active_session_id in stream_chat( | |
| user_message=message, | |
| services=get_services(), | |
| session_id=session_id, | |
| ): | |
| got_reply = True | |
| updated_history[-1] = {"role": "assistant", "content": partial_text} | |
| yield ( | |
| updated_history, | |
| active_session_id, | |
| "", | |
| "๋ต๋ณ ์์ฑ ์ค...", | |
| current_badge(message, partial_text), | |
| ) | |
| if not got_reply: | |
| updated_history[-1] = { | |
| "role": "assistant", | |
| "content": "์ค๋์ ๋ง๋ ฅ์ ํ๋ฆ์ด ์กฐ๊ธ ๋ถ์์ ํ๊ตฐ์. ์ ์ ํ ๋ค์ ์๋ํด ์ฃผ์๊ฒ ์ต๋๊น?", | |
| } | |
| yield updated_history, active_session_id, "", "", current_badge( | |
| message, | |
| updated_history[-1]["content"], | |
| ) | |
| with gr.Blocks(title="Megumin RAG Chat", fill_height=True) as demo: | |
| gr.HTML(BACKGROUND_HTML) | |
| with gr.Column(elem_classes=["shell"]): | |
| with gr.Row(elem_classes=["main-layout"]): | |
| with gr.Column(scale=4, min_width=400, elem_classes=["left-panel"]): | |
| gr.HTML(PROFILE_HTML) | |
| with gr.Column(scale=6, min_width=720, elem_classes=["chat-panel-col"]): | |
| gr.HTML(MOBILE_HERO_HTML) | |
| with gr.Group(elem_classes=["glass-panel", "chat-panel"]): | |
| gr.HTML( | |
| """ | |
| <div class="chat-header"> | |
| <div> | |
| <div class="eyebrow">Megumin Dialogue Chamber</div> | |
| <h1>๋ฉ๊ตฌ๋ฐ Chatbot</h1> | |
| <p>๋ฉ๊ตฌ๋ฐ์๊ฒ ์ง๋ฌธํ๊ณ , ์ค์ ๊ณผ ๊ฐ์ ์ ์ ๋ชจ๋ ๋ฐ์ํ ๋ต๋ณ์ ๋ฐ์๋ณด์ธ์.</p> | |
| </div> | |
| <div class="status-pill"> | |
| <span class="status-dot"></span> | |
| Try asking about Kazuma, Explosion, or Aqua | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| mood_badge = gr.HTML(current_badge(), elem_classes=["mood-badge-slot"]) | |
| with gr.Column(elem_classes=["chatbot-wrap"]): | |
| chatbot = gr.Chatbot( | |
| value=initial_history(), | |
| height=470, | |
| elem_classes=["chat-history"], | |
| render_markdown=True, | |
| buttons=["copy"], | |
| layout="bubble", | |
| label="Megumin Chat", | |
| ) | |
| runtime_status = gr.Markdown("", elem_classes=["runtime-status"]) | |
| session_state = gr.State(value=None) | |
| with gr.Column(elem_classes=["input-zone"]): | |
| with gr.Row(elem_classes=["input-row"]): | |
| with gr.Column(scale=8, min_width=0, elem_classes=["input-message"]): | |
| user_input = gr.Textbox( | |
| label="Message", | |
| placeholder="๋ฉ๊ตฌ๋ฐ์๊ฒ ๋ง์ ๊ฑธ์ด ๋ณด์ธ์.", | |
| lines=2, | |
| max_lines=4, | |
| ) | |
| with gr.Column(scale=2, min_width=110, elem_classes=["input-actions"]): | |
| send_button = gr.Button("Send", variant="primary") | |
| clear_button = gr.Button("Clear", variant="secondary") | |
| with gr.Column(scale=4, min_width=400, elem_classes=["right-panel"]): | |
| gr.HTML(VISUAL_HTML) | |
| submit_event = user_input.submit( | |
| fn=begin_request, | |
| inputs=[user_input, chatbot, session_state], | |
| outputs=[chatbot, session_state, user_input, runtime_status, mood_badge], | |
| ) | |
| submit_event.then( | |
| fn=respond, | |
| inputs=[user_input, chatbot, session_state], | |
| outputs=[chatbot, session_state, user_input, runtime_status, mood_badge], | |
| ) | |
| click_event = send_button.click( | |
| fn=begin_request, | |
| inputs=[user_input, chatbot, session_state], | |
| outputs=[chatbot, session_state, user_input, runtime_status, mood_badge], | |
| ) | |
| click_event.then( | |
| fn=respond, | |
| inputs=[user_input, chatbot, session_state], | |
| outputs=[chatbot, session_state, user_input, runtime_status, mood_badge], | |
| ) | |
| clear_button.click( | |
| fn=lambda: (initial_history(), None, "", "", current_badge()), | |
| inputs=None, | |
| outputs=[chatbot, session_state, user_input, runtime_status, mood_badge], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", css=CUSTOM_CSS, ssr_mode=False) | |