diff --git a/ROADMAP.md b/ROADMAP.md index 9959159..cc3f1fd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 - [ ] 다크 모드 토글 diff --git a/app.py b/app.py index acc96ed..146a50b 100644 --- a/app.py +++ b/app.py @@ -231,7 +231,7 @@ def _live_html(text: str) -> str: """스트리밍 중 현재 줄만 보여주는 단순 div (details 미사용 → 닫힘 현상 없음).""" return ( f'
' - f'🤔 분석 중...' + f'⏳ 분석 중...' f'
{_html.escape(text)} ▌
' f'
' ) @@ -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(""" +
+ 🤖 +
+
율봇
+
육아·금융 전문 AI 상담 도우미
+
+
+""") + 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(), )