UI/UX D2: branded header, custom theme, bubble styles, streaming animation, responsive

- app.py: styled HTML header with robot icon + subtitle (D2-11)
- app.py: gr.themes.Soft(blue/indigo/slate) applied to gr.Blocks (D2-12)
- app.py: user_selector moved to header Row, right-aligned scale=0 (D2-13)
- app.py: .message-wrap .user/.bot custom background + border-radius CSS (D2-14)
- app.py: streaming-indicator blink animation on _live_html (D2-15)
- app.py: @media max-width:768px responsive CSS (D2-16)
- ROADMAP.md: mark all D2 items complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 14:41:51 +09:00
parent c1a28bfdcc
commit fb438864b1
2 changed files with 71 additions and 18 deletions
+6 -5
View File
@@ -339,11 +339,12 @@ youlbot-webui/
- [x] `문서 수집` 버튼 크기 적정화 — `scale=0, min_width=200`
#### D2
- [ ] 헤더 아이콘/로고 추가
- [ ] `gr.themes.Soft()` 또는 커스텀 테마 적용
- [ ] 사용자 드롭다운 위치 이동
- [ ] 채팅 버블 커스텀 CSS
- [ ] 스트리밍 로딩 표시
- [x] 헤더 아이콘/로고 추가 — `gr.HTML` 🤖 아이콘 + 타이틀/서브타이틀 레이아웃
- [x] 커스텀 테마 적용 — `gr.themes.Soft(primary_hue="blue", secondary_hue="indigo", neutral_hue="slate")`
- [x] 사용자 드롭다운 위치 이동 — 탭 내부 → 헤더 Row 우측 (`scale=0, min_width=160`)
- [x] 채팅 버블 커스텀 CSS — `.message-wrap .user` 파란 배경, `.message-wrap .bot` 회색 배경
- [x] 스트리밍 로딩 표시 — `_live_html`에 `streaming-indicator` blink 애니메이션
- [x] 반응형 레이아웃 — `@media (max-width: 768px)` 모바일 대응 CSS
#### D3
- [ ] 다크 모드 토글
+65 -13
View File
@@ -231,7 +231,7 @@ def _live_html(text: str) -> str:
"""스트리밍 중 현재 줄만 보여주는 단순 div (details 미사용 → 닫힘 현상 없음)."""
return (
f'<div style="{_BOX_STYLE}">'
f'<strong>🤔 분석 중...</strong>'
f'<strong class="streaming-indicator">⏳ 분석 중...</strong>'
f'<div style="{_CONTENT_STYLE}">{_html.escape(text)} ▌</div>'
f'</div>'
)
@@ -274,26 +274,79 @@ def _sources_html(sources: list) -> str:
_CUSTOM_CSS = """
footer { display: none !important; }
/* 입력 영역 */
.send-btn { min-height: 80px !important; align-self: stretch !important; }
/* 헤더 (D2-11) */
.app-header {
align-items: center !important;
padding-bottom: 12px !important;
border-bottom: 1px solid var(--border-color-primary);
margin-bottom: 4px !important;
}
.app-header > .wrap, .app-header > div > .wrap {
padding: 0 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
/* 채팅 버블 스타일 (D2-14) */
.message-wrap .user {
background: #dbeafe !important;
border-color: #93c5fd !important;
border-bottom-right-radius: 4px !important;
}
.message-wrap .bot {
background: #f8fafc !important;
border-color: #e2e8f0 !important;
border-bottom-left-radius: 4px !important;
}
/* 응답 스트리밍 애니메이션 (D2-15) */
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.streaming-indicator { animation: blink 1.2s ease-in-out infinite; display: inline-block; }
/* 반응형 (D2-16) */
@media (max-width: 768px) {
.send-btn { min-height: 56px !important; }
.app-header { flex-wrap: wrap; gap: 8px; }
.message-wrap .user, .message-wrap .bot { max-width: 92% !important; }
}
"""
with gr.Blocks(title="율봇", css=_CUSTOM_CSS) as demo:
gr.Markdown("# 율봇\n육아·금융 전문 AI 상담 도우미")
_THEME = gr.themes.Soft(
primary_hue="blue",
secondary_hue="indigo",
neutral_hue="slate",
)
with gr.Blocks(title="율봇", css=_CUSTOM_CSS, theme=_THEME) as demo:
with gr.Row(elem_classes=["app-header"]):
gr.HTML("""
<div style="display:flex;align-items:center;gap:14px;padding:4px 0;">
<span style="font-size:2.2rem;line-height:1;">🤖</span>
<div>
<div style="font-size:1.6rem;font-weight:700;color:#1e293b;line-height:1.15;">율봇</div>
<div style="font-size:0.85rem;color:#64748b;margin-top:3px;">육아·금융 전문 AI 상담 도우미</div>
</div>
</div>
""")
user_selector = gr.Dropdown(
choices=USER_LABELS,
value=DEFAULT_USER,
label="사용자",
scale=0,
min_width=160,
)
user_state = gr.State(DEFAULT_USER)
run_ids_state = gr.State([])
with gr.Tab("대화"):
with gr.Row():
user_selector = gr.Dropdown(
choices=USER_LABELS,
value=DEFAULT_USER,
label="사용자",
scale=1,
)
thinking_box = gr.HTML(value="")
chatbot = gr.Chatbot(label="율봇", height=500)
chatbot = gr.Chatbot(label="율봇", height=500, elem_id="main-chatbot")
source_box = gr.HTML(value="")
with gr.Row():
msg_box = gr.Textbox(
@@ -406,5 +459,4 @@ if __name__ == "__main__":
demo.launch(
server_name=container.config().server_host,
server_port=container.config().server_port,
theme=gr.themes.Soft(),
)