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:
+6
-5
@@ -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
|
||||
- [ ] 다크 모드 토글
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user