Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- README.md +13 -1
- app_gradio_messenger.py +592 -0
README.md
CHANGED
|
@@ -23,7 +23,7 @@ pinned: false
|
|
| 23 |
- ์์ฟ ์์ ์ธ์ดํฌ๋ฆฌ๋ ๋ธ๋ ์ดํฌ ์คํ ์ด ๋ฌด์์ธ์ง ์ค๋ช
ํด ์ฃผ์ธ์.
|
| 24 |
- ํ๋ง์กฑ๋ค์ด ์๊ธฐ์๊ฐ๋ฅผ ํ ๋ฒ ๋ค๋ ค์ฃผ์๊ฒ ์ต๋๊น?
|
| 25 |
|
| 26 |
-
์ฒซ ์ ์ ์ ๋ฉ๊ตฌ๋ฐ์ด ๋จผ์ ์ธ์ฟ๋ง์ ๊ฑด
|
| 27 |
|
| 28 |
## ์ด Space์ ํน์ง
|
| 29 |
|
|
@@ -42,12 +42,24 @@ pinned: false
|
|
| 42 |
python app_gradio.py
|
| 43 |
```
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
Hugging Face Spaces ์ง์
์ :
|
| 46 |
|
| 47 |
```bash
|
| 48 |
python app.py
|
| 49 |
```
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
## Hugging Face ๊ตฌ์ฑ
|
| 52 |
|
| 53 |
์ฑ repo:
|
|
|
|
| 23 |
- ์์ฟ ์์ ์ธ์ดํฌ๋ฆฌ๋ ๋ธ๋ ์ดํฌ ์คํ ์ด ๋ฌด์์ธ์ง ์ค๋ช
ํด ์ฃผ์ธ์.
|
| 24 |
- ํ๋ง์กฑ๋ค์ด ์๊ธฐ์๊ฐ๋ฅผ ํ ๋ฒ ๋ค๋ ค์ฃผ์๊ฒ ์ต๋๊น?
|
| 25 |
|
| 26 |
+
์ฒซ ์ ์ ์ ๋ฉ๊ตฌ๋ฐ์ด ๋จผ์ ์ธ์ฟ๋ง์ ๊ฑด๋ค๊ณ , ๋ต๋ณ์ ADK SSE ์คํธ๋ฆฌ๋ฐ์ผ๋ก ์ ์ง์ ์ผ๋ก ํ์๋ฉ๋๋ค.
|
| 27 |
|
| 28 |
## ์ด Space์ ํน์ง
|
| 29 |
|
|
|
|
| 42 |
python app_gradio.py
|
| 43 |
```
|
| 44 |
|
| 45 |
+
๋ฉ์ ์ ํ UI:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
python app_gradio_messenger.py
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
Hugging Face Spaces ์ง์
์ :
|
| 52 |
|
| 53 |
```bash
|
| 54 |
python app.py
|
| 55 |
```
|
| 56 |
|
| 57 |
+
UI ์ ํ:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
MEGUMIN_UI=messenger
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
## Hugging Face ๊ตฌ์ฑ
|
| 64 |
|
| 65 |
์ฑ repo:
|
app_gradio_messenger.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import html
|
| 4 |
+
import os
|
| 5 |
+
from threading import Lock
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
|
| 9 |
+
from megumin_agent.chat import ChatServices
|
| 10 |
+
from megumin_agent.chat import create_chat_services
|
| 11 |
+
from megumin_agent.chat import stream_chat
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
INITIAL_GREETING = "๋ด ์ด๋ฆ์ ๋ฉ๊ตฌ๋ฐ! ํ๋ง์กฑ ์ ์ผ์ ๋ง๋ฒ์ฌ์ด์, ํญ๋ ฌ ๋ง๋ฒ์ ํผ์น๋ ์!"
|
| 15 |
+
INITIAL_HISTORY = [{"role": "assistant", "content": INITIAL_GREETING}]
|
| 16 |
+
|
| 17 |
+
SERVICES: ChatServices | None = None
|
| 18 |
+
SERVICES_LOCK = Lock()
|
| 19 |
+
MEGUMIN_IMAGE_URL = os.getenv(
|
| 20 |
+
"MEGUMIN_IMAGE_URL",
|
| 21 |
+
"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",
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
MEGUMIN_SVG = """
|
| 25 |
+
<svg viewBox="0 0 420 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Megumin inspired illustration">
|
| 26 |
+
<defs>
|
| 27 |
+
<linearGradient id="cape" x1="0" y1="0" x2="1" y2="1">
|
| 28 |
+
<stop offset="0%" stop-color="#7b0f1b"/>
|
| 29 |
+
<stop offset="100%" stop-color="#2c0610"/>
|
| 30 |
+
</linearGradient>
|
| 31 |
+
<linearGradient id="hat" x1="0" y1="0" x2="1" y2="1">
|
| 32 |
+
<stop offset="0%" stop-color="#41221f"/>
|
| 33 |
+
<stop offset="100%" stop-color="#120b12"/>
|
| 34 |
+
</linearGradient>
|
| 35 |
+
<linearGradient id="gold" x1="0" y1="0" x2="1" y2="1">
|
| 36 |
+
<stop offset="0%" stop-color="#ffd86a"/>
|
| 37 |
+
<stop offset="100%" stop-color="#e59a22"/>
|
| 38 |
+
</linearGradient>
|
| 39 |
+
</defs>
|
| 40 |
+
<ellipse cx="212" cy="520" rx="120" ry="22" fill="rgba(20,6,10,0.28)"/>
|
| 41 |
+
<circle cx="300" cy="104" r="24" fill="rgba(255,114,61,0.28)"/>
|
| 42 |
+
<circle cx="334" cy="138" r="12" fill="rgba(255,201,115,0.32)"/>
|
| 43 |
+
<path d="M132 180 C116 258 106 344 114 454 L306 454 C314 344 304 258 288 180 Z" fill="url(#cape)"/>
|
| 44 |
+
<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"/>
|
| 45 |
+
<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"/>
|
| 46 |
+
<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)"/>
|
| 47 |
+
<path d="M120 138 L84 154 L108 178 L142 166 Z" fill="#d33b2d"/>
|
| 48 |
+
<path d="M310 142 L344 158 L320 180 L286 168 Z" fill="#d33b2d"/>
|
| 49 |
+
<path d="M170 274 C194 288 226 290 250 274" stroke="#6a2d1b" stroke-width="8" stroke-linecap="round" fill="none"/>
|
| 50 |
+
<circle cx="180" cy="210" r="11" fill="#4a1d14"/>
|
| 51 |
+
<circle cx="242" cy="210" r="11" fill="#4a1d14"/>
|
| 52 |
+
<path d="M132 108 L170 70 L206 116 Z" fill="url(#gold)"/>
|
| 53 |
+
<circle cx="164" cy="92" r="16" fill="#c71f2d"/>
|
| 54 |
+
<path d="M246 78 L340 28 L350 54 L264 96 Z" fill="#5a2f1f"/>
|
| 55 |
+
<circle cx="338" cy="32" r="16" fill="url(#gold)"/>
|
| 56 |
+
<path d="M292 186 L372 78 L388 92 L318 196 Z" fill="#5a2f1f"/>
|
| 57 |
+
<path d="M372 74 L388 48 L402 86 Z" fill="#ff8b2f"/>
|
| 58 |
+
<path d="M318 196 L364 250" stroke="#5a2f1f" stroke-width="12" stroke-linecap="round"/>
|
| 59 |
+
<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"/>
|
| 60 |
+
<path d="M160 456 L188 300 L232 300 L260 456 Z" fill="#23131a"/>
|
| 61 |
+
<path d="M154 302 L114 454" stroke="#120b12" stroke-width="18" stroke-linecap="round"/>
|
| 62 |
+
<path d="M266 302 L306 454" stroke="#120b12" stroke-width="18" stroke-linecap="round"/>
|
| 63 |
+
</svg>
|
| 64 |
+
""".strip()
|
| 65 |
+
|
| 66 |
+
CUSTOM_CSS = """
|
| 67 |
+
:root {
|
| 68 |
+
--messenger-bg: #1a0d14;
|
| 69 |
+
--messenger-panel: rgba(255, 247, 238, 0.1);
|
| 70 |
+
--messenger-panel-strong: rgba(255, 247, 238, 0.14);
|
| 71 |
+
--messenger-line: rgba(255, 213, 158, 0.16);
|
| 72 |
+
--messenger-shadow: 0 18px 50px rgba(8, 2, 4, 0.34);
|
| 73 |
+
--user-bubble: #ffe6c7;
|
| 74 |
+
--assistant-bubble: #fff7f0;
|
| 75 |
+
--assistant-text: #1f1820;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
body, .gradio-container {
|
| 79 |
+
background:
|
| 80 |
+
radial-gradient(circle at 12% 16%, rgba(255, 145, 71, 0.14), transparent 26%),
|
| 81 |
+
radial-gradient(circle at 82% 8%, rgba(217, 47, 78, 0.16), transparent 22%),
|
| 82 |
+
linear-gradient(135deg, #1a0d14 0%, #12070d 52%, #221520 100%);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.gradio-container {
|
| 86 |
+
max-width: 1680px !important;
|
| 87 |
+
padding: 18px !important;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.messenger-shell {
|
| 91 |
+
min-height: calc(100vh - 36px);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.messenger-layout {
|
| 95 |
+
display: grid;
|
| 96 |
+
grid-template-columns: 3fr 7fr;
|
| 97 |
+
gap: 18px;
|
| 98 |
+
min-height: calc(100vh - 36px);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.messenger-panel {
|
| 102 |
+
background: linear-gradient(180deg, var(--messenger-panel-strong), var(--messenger-panel));
|
| 103 |
+
border: 1px solid var(--messenger-line);
|
| 104 |
+
border-radius: 24px;
|
| 105 |
+
box-shadow: var(--messenger-shadow);
|
| 106 |
+
backdrop-filter: blur(12px);
|
| 107 |
+
overflow: hidden;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.sidebar {
|
| 111 |
+
display: flex;
|
| 112 |
+
flex-direction: column;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.sidebar-head {
|
| 116 |
+
padding: 18px 18px 12px;
|
| 117 |
+
border-bottom: 1px solid var(--messenger-line);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.sidebar-head h1 {
|
| 121 |
+
margin: 0;
|
| 122 |
+
font-size: 1.25rem;
|
| 123 |
+
color: #fff8ef;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.sidebar-head p {
|
| 127 |
+
margin: 8px 0 0;
|
| 128 |
+
color: rgba(255, 241, 227, 0.8);
|
| 129 |
+
font-size: 0.92rem;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.profile-list {
|
| 133 |
+
padding: 12px;
|
| 134 |
+
display: flex;
|
| 135 |
+
flex-direction: column;
|
| 136 |
+
gap: 10px;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.profile-card {
|
| 140 |
+
display: grid;
|
| 141 |
+
grid-template-columns: 56px 1fr;
|
| 142 |
+
gap: 12px;
|
| 143 |
+
align-items: center;
|
| 144 |
+
padding: 10px;
|
| 145 |
+
border-radius: 16px;
|
| 146 |
+
background: rgba(255, 248, 239, 0.08);
|
| 147 |
+
border: 1px solid rgba(255, 229, 194, 0.12);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.profile-avatar {
|
| 151 |
+
width: 56px;
|
| 152 |
+
height: 56px;
|
| 153 |
+
border-radius: 14px;
|
| 154 |
+
overflow: hidden;
|
| 155 |
+
background: rgba(255, 248, 239, 0.1);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.profile-avatar img,
|
| 159 |
+
.profile-avatar .fallback {
|
| 160 |
+
width: 100%;
|
| 161 |
+
height: 100%;
|
| 162 |
+
display: block;
|
| 163 |
+
object-fit: cover;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.profile-title {
|
| 167 |
+
color: #fff9f2;
|
| 168 |
+
font-weight: 700;
|
| 169 |
+
font-size: 0.98rem;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.profile-snippet {
|
| 173 |
+
color: rgba(255, 240, 226, 0.72);
|
| 174 |
+
font-size: 0.84rem;
|
| 175 |
+
margin-top: 4px;
|
| 176 |
+
line-height: 1.4;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.chat-stage {
|
| 180 |
+
display: grid;
|
| 181 |
+
grid-template-rows: auto 1fr auto;
|
| 182 |
+
min-height: calc(100vh - 36px);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.chat-head {
|
| 186 |
+
padding: 18px 20px 14px;
|
| 187 |
+
border-bottom: 1px solid var(--messenger-line);
|
| 188 |
+
display: flex;
|
| 189 |
+
justify-content: space-between;
|
| 190 |
+
align-items: center;
|
| 191 |
+
gap: 12px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.chat-head-main {
|
| 195 |
+
display: grid;
|
| 196 |
+
grid-template-columns: 52px 1fr;
|
| 197 |
+
gap: 12px;
|
| 198 |
+
align-items: center;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.chat-head-avatar {
|
| 202 |
+
width: 52px;
|
| 203 |
+
height: 52px;
|
| 204 |
+
border-radius: 16px;
|
| 205 |
+
overflow: hidden;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.chat-head-avatar img,
|
| 209 |
+
.chat-head-avatar .fallback {
|
| 210 |
+
width: 100%;
|
| 211 |
+
height: 100%;
|
| 212 |
+
display: block;
|
| 213 |
+
object-fit: cover;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.chat-head-title {
|
| 217 |
+
color: #fff8ef;
|
| 218 |
+
font-size: 1.08rem;
|
| 219 |
+
font-weight: 700;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.chat-head-desc {
|
| 223 |
+
color: rgba(255, 239, 223, 0.75);
|
| 224 |
+
font-size: 0.86rem;
|
| 225 |
+
margin-top: 4px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.chat-status {
|
| 229 |
+
color: rgba(255, 233, 203, 0.86);
|
| 230 |
+
font-size: 0.82rem;
|
| 231 |
+
padding: 8px 12px;
|
| 232 |
+
border-radius: 999px;
|
| 233 |
+
background: rgba(255, 239, 219, 0.08);
|
| 234 |
+
border: 1px solid rgba(255, 219, 169, 0.12);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.chat-history {
|
| 238 |
+
padding: 18px 18px 8px;
|
| 239 |
+
overflow-y: auto;
|
| 240 |
+
min-height: 0;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.message-col {
|
| 244 |
+
display: flex;
|
| 245 |
+
flex-direction: column;
|
| 246 |
+
gap: 12px;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.message-row {
|
| 250 |
+
display: flex;
|
| 251 |
+
width: 100%;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.message-row.assistant {
|
| 255 |
+
justify-content: flex-start;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.message-row.user {
|
| 259 |
+
justify-content: flex-end;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.assistant-wrap {
|
| 263 |
+
display: grid;
|
| 264 |
+
grid-template-columns: 42px minmax(0, 1fr);
|
| 265 |
+
gap: 10px;
|
| 266 |
+
max-width: 78%;
|
| 267 |
+
align-items: start;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.assistant-avatar {
|
| 271 |
+
width: 42px;
|
| 272 |
+
height: 42px;
|
| 273 |
+
border-radius: 12px;
|
| 274 |
+
overflow: hidden;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.assistant-avatar img,
|
| 278 |
+
.assistant-avatar .fallback {
|
| 279 |
+
width: 100%;
|
| 280 |
+
height: 100%;
|
| 281 |
+
display: block;
|
| 282 |
+
object-fit: cover;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.bubble {
|
| 286 |
+
padding: 12px 14px;
|
| 287 |
+
border-radius: 18px;
|
| 288 |
+
line-height: 1.6;
|
| 289 |
+
font-size: 0.95rem;
|
| 290 |
+
white-space: pre-wrap;
|
| 291 |
+
word-break: break-word;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.assistant-bubble {
|
| 295 |
+
background: var(--assistant-bubble);
|
| 296 |
+
color: var(--assistant-text);
|
| 297 |
+
border-top-left-radius: 8px;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.user-bubble {
|
| 301 |
+
max-width: 72%;
|
| 302 |
+
background: var(--user-bubble);
|
| 303 |
+
color: #2f1e1a;
|
| 304 |
+
border-top-right-radius: 8px;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.chat-compose {
|
| 308 |
+
padding: 14px 16px 16px;
|
| 309 |
+
border-top: 1px solid var(--messenger-line);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.compose-status {
|
| 313 |
+
margin-bottom: 10px;
|
| 314 |
+
color: rgba(255, 239, 223, 0.82);
|
| 315 |
+
font-size: 0.86rem;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.compose-row {
|
| 319 |
+
display: grid;
|
| 320 |
+
grid-template-columns: 1fr 112px;
|
| 321 |
+
gap: 10px;
|
| 322 |
+
align-items: stretch;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.compose-buttons {
|
| 326 |
+
display: flex;
|
| 327 |
+
flex-direction: column;
|
| 328 |
+
gap: 10px;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.compose-row textarea,
|
| 332 |
+
.compose-row input {
|
| 333 |
+
color: #111111 !important;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.compose-row .gr-textbox,
|
| 337 |
+
.compose-row .gr-button,
|
| 338 |
+
.compose-row .gr-form {
|
| 339 |
+
background: rgba(255, 248, 239, 0.08) !important;
|
| 340 |
+
border-color: rgba(255, 226, 186, 0.16) !important;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.compose-buttons .gr-button {
|
| 344 |
+
min-height: 46px;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.compose-row .gr-button-primary {
|
| 348 |
+
background: linear-gradient(135deg, #f19f35 0%, #d84f34 100%) !important;
|
| 349 |
+
color: #fff8ef !important;
|
| 350 |
+
border: none !important;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.compose-row .gr-button-secondary {
|
| 354 |
+
color: #fff8ef !important;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
@media (max-width: 900px) {
|
| 358 |
+
.messenger-layout {
|
| 359 |
+
grid-template-columns: 1fr;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.sidebar {
|
| 363 |
+
min-height: auto;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.chat-stage {
|
| 367 |
+
min-height: 70vh;
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
""".strip()
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def get_services() -> ChatServices:
|
| 374 |
+
global SERVICES
|
| 375 |
+
if SERVICES is None:
|
| 376 |
+
with SERVICES_LOCK:
|
| 377 |
+
if SERVICES is None:
|
| 378 |
+
SERVICES = create_chat_services()
|
| 379 |
+
return SERVICES
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def initial_history() -> list[dict[str, str]]:
|
| 383 |
+
return [dict(item) for item in INITIAL_HISTORY]
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def truncate_text(text: str, limit: int = 34) -> str:
|
| 387 |
+
compact = " ".join(str(text or "").split()).strip()
|
| 388 |
+
if len(compact) <= limit:
|
| 389 |
+
return compact
|
| 390 |
+
return compact[: limit - 3].rstrip() + "..."
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def render_avatar(size_class: str) -> str:
|
| 394 |
+
return f"""
|
| 395 |
+
<div class="{size_class}">
|
| 396 |
+
<img src="{MEGUMIN_IMAGE_URL}" alt="Megumin" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';" />
|
| 397 |
+
<div class="fallback">{MEGUMIN_SVG}</div>
|
| 398 |
+
</div>
|
| 399 |
+
""".strip()
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def render_sidebar(history: list[dict[str, str]]) -> str:
|
| 403 |
+
last_message = next(
|
| 404 |
+
(item["content"] for item in reversed(history) if item.get("content")),
|
| 405 |
+
INITIAL_GREETING,
|
| 406 |
+
)
|
| 407 |
+
return f"""
|
| 408 |
+
<div class="sidebar-head">
|
| 409 |
+
<h1>Characters</h1>
|
| 410 |
+
<p>ํ์ฌ ๋ํ ์ค์ธ ์บ๋ฆญํฐ๋ฅผ ์ฌ๊ธฐ์์ ํ์ธํ ์ ์์ต๋๋ค.</p>
|
| 411 |
+
</div>
|
| 412 |
+
<div class="profile-list">
|
| 413 |
+
<div class="profile-card">
|
| 414 |
+
{render_avatar("profile-avatar")}
|
| 415 |
+
<div>
|
| 416 |
+
<div class="profile-title">๋ฉ๊ตฌ๋ฐ</div>
|
| 417 |
+
<div class="profile-snippet">{html.escape(truncate_text(last_message))}</div>
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
</div>
|
| 421 |
+
""".strip()
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def render_messages(history: list[dict[str, str]]) -> str:
|
| 425 |
+
rows: list[str] = []
|
| 426 |
+
for item in history:
|
| 427 |
+
role = item.get("role", "")
|
| 428 |
+
content = html.escape(item.get("content", ""))
|
| 429 |
+
if role == "assistant":
|
| 430 |
+
rows.append(
|
| 431 |
+
f"""
|
| 432 |
+
<div class="message-row assistant">
|
| 433 |
+
<div class="assistant-wrap">
|
| 434 |
+
{render_avatar("assistant-avatar")}
|
| 435 |
+
<div class="bubble assistant-bubble">{content}</div>
|
| 436 |
+
</div>
|
| 437 |
+
</div>
|
| 438 |
+
""".strip()
|
| 439 |
+
)
|
| 440 |
+
else:
|
| 441 |
+
rows.append(
|
| 442 |
+
f"""
|
| 443 |
+
<div class="message-row user">
|
| 444 |
+
<div class="bubble user-bubble">{content}</div>
|
| 445 |
+
</div>
|
| 446 |
+
""".strip()
|
| 447 |
+
)
|
| 448 |
+
return f'<div class="message-col">{"".join(rows)}</div>'
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def begin_request(
|
| 452 |
+
message: str,
|
| 453 |
+
history: list[dict[str, str]],
|
| 454 |
+
session_id: str | None,
|
| 455 |
+
):
|
| 456 |
+
if not message.strip():
|
| 457 |
+
return render_sidebar(history), render_messages(history), history, session_id, "", ""
|
| 458 |
+
status_text = "์๋น์ค ์ค๋น ์ค..." if SERVICES is None else "๋ต๋ณ ์์ฑ ์ค..."
|
| 459 |
+
return render_sidebar(history), render_messages(history), history, session_id, message, status_text
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
async def respond(
|
| 463 |
+
message: str,
|
| 464 |
+
history: list[dict[str, str]],
|
| 465 |
+
session_id: str | None,
|
| 466 |
+
):
|
| 467 |
+
if not message.strip():
|
| 468 |
+
yield render_sidebar(history), render_messages(history), history, session_id, "", ""
|
| 469 |
+
return
|
| 470 |
+
|
| 471 |
+
updated_history = list(history)
|
| 472 |
+
updated_history.append({"role": "user", "content": message})
|
| 473 |
+
updated_history.append({"role": "assistant", "content": ""})
|
| 474 |
+
yield (
|
| 475 |
+
render_sidebar(updated_history),
|
| 476 |
+
render_messages(updated_history),
|
| 477 |
+
updated_history,
|
| 478 |
+
session_id,
|
| 479 |
+
"",
|
| 480 |
+
"๋ต๋ณ ์์ฑ ์ค...",
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
active_session_id = session_id
|
| 484 |
+
got_reply = False
|
| 485 |
+
async for partial_text, active_session_id in stream_chat(
|
| 486 |
+
user_message=message,
|
| 487 |
+
services=get_services(),
|
| 488 |
+
session_id=session_id,
|
| 489 |
+
):
|
| 490 |
+
got_reply = True
|
| 491 |
+
updated_history[-1] = {"role": "assistant", "content": partial_text}
|
| 492 |
+
yield (
|
| 493 |
+
render_sidebar(updated_history),
|
| 494 |
+
render_messages(updated_history),
|
| 495 |
+
updated_history,
|
| 496 |
+
active_session_id,
|
| 497 |
+
"",
|
| 498 |
+
"๋ต๋ณ ์์ฑ ์ค...",
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
if not got_reply:
|
| 502 |
+
updated_history[-1] = {
|
| 503 |
+
"role": "assistant",
|
| 504 |
+
"content": "์ค๋์ ๋ง๋ ฅ์ ํ๋ฆ์ด ์กฐ๊ธ ๋ถ์์ ํ๊ตฐ์. ์ ์ ํ ๋ค์ ์๋ํด ์ฃผ์๊ฒ ์ต๋๊น?",
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
yield (
|
| 508 |
+
render_sidebar(updated_history),
|
| 509 |
+
render_messages(updated_history),
|
| 510 |
+
updated_history,
|
| 511 |
+
active_session_id,
|
| 512 |
+
"",
|
| 513 |
+
"",
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
with gr.Blocks(title="Megumin Messenger", fill_height=True) as demo:
|
| 518 |
+
history_state = gr.State(value=initial_history())
|
| 519 |
+
session_state = gr.State(value=None)
|
| 520 |
+
|
| 521 |
+
with gr.Column(elem_classes=["messenger-shell"]):
|
| 522 |
+
with gr.Row(elem_classes=["messenger-layout"]):
|
| 523 |
+
with gr.Column(elem_classes=["messenger-panel", "sidebar"]):
|
| 524 |
+
sidebar_html = gr.HTML(render_sidebar(initial_history()))
|
| 525 |
+
|
| 526 |
+
with gr.Column(elem_classes=["messenger-panel", "chat-stage"]):
|
| 527 |
+
gr.HTML(
|
| 528 |
+
f"""
|
| 529 |
+
<div class="chat-head">
|
| 530 |
+
<div class="chat-head-main">
|
| 531 |
+
{render_avatar("chat-head-avatar")}
|
| 532 |
+
<div>
|
| 533 |
+
<div class="chat-head-title">๋ฉ๊ตฌ๋ฐ</div>
|
| 534 |
+
<div class="chat-head-desc">ํ๋ง์กฑ ์ต๊ณ ์ ํญ๋ ฌ๋ง๋ฒ์ฌ์ ๋ฉ์ ์ ์ฒ๋ผ ๋ํํด๋ณด์ธ์.</div>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
<div class="chat-status">Megumin Messenger</div>
|
| 538 |
+
</div>
|
| 539 |
+
""".strip()
|
| 540 |
+
)
|
| 541 |
+
messages_html = gr.HTML(render_messages(initial_history()), elem_classes=["chat-history"])
|
| 542 |
+
with gr.Column(elem_classes=["chat-compose"]):
|
| 543 |
+
runtime_status = gr.Markdown("", elem_classes=["compose-status"])
|
| 544 |
+
with gr.Row(elem_classes=["compose-row"]):
|
| 545 |
+
user_input = gr.Textbox(
|
| 546 |
+
label="Message",
|
| 547 |
+
placeholder="๋ฉ๏ฟฝ๏ฟฝ๋ฐ์๊ฒ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ณด์ธ์.",
|
| 548 |
+
lines=3,
|
| 549 |
+
max_lines=5,
|
| 550 |
+
)
|
| 551 |
+
with gr.Column(elem_classes=["compose-buttons"]):
|
| 552 |
+
send_button = gr.Button("Send", variant="primary")
|
| 553 |
+
clear_button = gr.Button("Clear", variant="secondary")
|
| 554 |
+
|
| 555 |
+
submit_event = user_input.submit(
|
| 556 |
+
fn=begin_request,
|
| 557 |
+
inputs=[user_input, history_state, session_state],
|
| 558 |
+
outputs=[sidebar_html, messages_html, history_state, session_state, user_input, runtime_status],
|
| 559 |
+
)
|
| 560 |
+
submit_event.then(
|
| 561 |
+
fn=respond,
|
| 562 |
+
inputs=[user_input, history_state, session_state],
|
| 563 |
+
outputs=[sidebar_html, messages_html, history_state, session_state, user_input, runtime_status],
|
| 564 |
+
)
|
| 565 |
+
|
| 566 |
+
click_event = send_button.click(
|
| 567 |
+
fn=begin_request,
|
| 568 |
+
inputs=[user_input, history_state, session_state],
|
| 569 |
+
outputs=[sidebar_html, messages_html, history_state, session_state, user_input, runtime_status],
|
| 570 |
+
)
|
| 571 |
+
click_event.then(
|
| 572 |
+
fn=respond,
|
| 573 |
+
inputs=[user_input, history_state, session_state],
|
| 574 |
+
outputs=[sidebar_html, messages_html, history_state, session_state, user_input, runtime_status],
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
clear_button.click(
|
| 578 |
+
fn=lambda: (
|
| 579 |
+
render_sidebar(initial_history()),
|
| 580 |
+
render_messages(initial_history()),
|
| 581 |
+
initial_history(),
|
| 582 |
+
None,
|
| 583 |
+
"",
|
| 584 |
+
"",
|
| 585 |
+
),
|
| 586 |
+
inputs=None,
|
| 587 |
+
outputs=[sidebar_html, messages_html, history_state, session_state, user_input, runtime_status],
|
| 588 |
+
)
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
if __name__ == "__main__":
|
| 592 |
+
demo.launch(server_name="0.0.0.0", css=CUSTOM_CSS, ssr_mode=False)
|