feat: add Gemini/ChatGPT-style 3-dot bounce loading indicator
- _loading_html(): 3 blue-to-indigo gradient dots with bounce animation - _status_html(): delegates to _loading_html for consistent style - _live_html(): shows dots + label during thinking/meta streaming - respond(): first yield shows _loading_html() immediately on send - CSS: @keyframes dot-bounce (translateY -7px scale 1.05 at 30%) - fix: move css/theme/js from gr.Blocks() to launch() (Gradio 6 API) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,8 +64,8 @@ async def respond(message, history, show_thinking, user_id, use_tts, run_ids, im
|
||||
display_msg = f"🖼️ [이미지 첨부]\n{message}" if message.strip() else "🖼️ [이미지 첨부]"
|
||||
history.append({"role": "user", "content": display_msg})
|
||||
history.append({"role": "assistant", "content": ""})
|
||||
# 첫 메시지 → 웰컴 뷰 숨기고 챗봇 컬럼 표시 (D4-2)
|
||||
yield history, "", None, run_ids, "", "", None, gr.update(visible=False), gr.update(visible=True)
|
||||
# 첫 메시지 → 웰컴 뷰 숨기고 챗봇 컬럼 표시 + 로딩 인디케이터
|
||||
yield history, "", None, run_ids, _loading_html(), "", None, gr.update(visible=False), gr.update(visible=True)
|
||||
|
||||
collected_run_id = None
|
||||
tts_text = ""
|
||||
@@ -248,11 +248,29 @@ def _last_line(text: str) -> str:
|
||||
return lines[-1] if lines else text.strip()
|
||||
|
||||
|
||||
def _loading_html(label: str = "율봇이 생각하고 있어요") -> str:
|
||||
"""Gemini/ChatGPT 스타일 3-dot 바운스 로딩 인디케이터"""
|
||||
return (
|
||||
f'<div style="display:flex;align-items:center;gap:12px;'
|
||||
f'padding:14px 18px;background:#f8fafc;border-radius:14px;margin-bottom:8px;">'
|
||||
f'<div class="loading-dots">'
|
||||
f'<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
|
||||
f'</div>'
|
||||
f'<span style="font-size:.88rem;color:#64748b;">{_html.escape(label)}</span>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
|
||||
def _live_html(text: str) -> str:
|
||||
return (
|
||||
f'<div style="{_BOX_STYLE}">'
|
||||
f'<strong class="streaming-indicator">⏳ 분석 중...</strong>'
|
||||
f'<div style="{_CONTENT_STYLE}">{_html.escape(text)} ▌</div>'
|
||||
f'<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">'
|
||||
f'<div class="loading-dots">'
|
||||
f'<span class="dot"></span><span class="dot"></span><span class="dot"></span>'
|
||||
f'</div>'
|
||||
f'<strong style="font-size:.88rem;color:#64748b;">분석 중...</strong>'
|
||||
f'</div>'
|
||||
f'<div style="{_CONTENT_STYLE}">{_html.escape(text)}</div>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
@@ -267,11 +285,7 @@ def _thinking_html(text: str) -> str:
|
||||
|
||||
|
||||
def _status_html(status: str) -> str:
|
||||
return (
|
||||
f'<div style="{_BOX_STYLE}">'
|
||||
f'<strong>🤔 {_html.escape(status)}</strong>'
|
||||
f'</div>'
|
||||
)
|
||||
return _loading_html(status)
|
||||
|
||||
|
||||
def _sources_html(sources: list) -> str:
|
||||
@@ -463,6 +477,20 @@ _JS = """
|
||||
_CUSTOM_CSS = """
|
||||
footer { display: none !important; }
|
||||
|
||||
/* ── 로딩 인디케이터 (Gemini/ChatGPT 3-dot 바운스) ── */
|
||||
.loading-dots { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; }
|
||||
.loading-dots .dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #818cf8 100%);
|
||||
animation: dot-bounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
.loading-dots .dot:nth-child(2) { animation-delay: .18s; }
|
||||
.loading-dots .dot:nth-child(3) { animation-delay: .36s; }
|
||||
@keyframes dot-bounce {
|
||||
0%, 60%, 100% { transform: translateY(0) scale(0.65); opacity: 0.35; }
|
||||
30% { transform: translateY(-7px) scale(1.05); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 스트리밍 중 파란 테두리 제거 (.generating 클래스가 원인) */
|
||||
.wrap.generating {
|
||||
border-color: var(--border-color-primary) !important;
|
||||
@@ -726,7 +754,7 @@ _THEME = gr.themes.Soft(
|
||||
|
||||
# ── UI 구성 ───────────────────────────────────────────────────────
|
||||
|
||||
with gr.Blocks(title="율봇", css=_CUSTOM_CSS, theme=_THEME, js=_JS) as demo:
|
||||
with gr.Blocks(title="율봇") as demo:
|
||||
|
||||
user_state = gr.State(DEFAULT_USER)
|
||||
run_ids_state = gr.State([])
|
||||
@@ -925,4 +953,7 @@ if __name__ == "__main__":
|
||||
demo.launch(
|
||||
server_name=container.config().server_host,
|
||||
server_port=container.config().server_port,
|
||||
theme=_THEME,
|
||||
css=_CUSTOM_CSS,
|
||||
js=_JS,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user