diff --git a/app.py b/app.py index 103de3d..e9e04fb 100644 --- a/app.py +++ b/app.py @@ -8,6 +8,7 @@ YOULBOT_API_TOKEN= ← api.py에 API_TOKEN 설정 시 동일 값 """ import asyncio +import html as _html import os import platform import subprocess @@ -95,19 +96,21 @@ async def tts_speak(text: str) -> str | None: async def respond(message, history, show_thinking, user_id, use_tts, run_ids): if not message.strip(): - yield history, "", None, run_ids, gr.update() + yield history, "", None, run_ids, "", "" return history = list(history) run_ids = list(run_ids) history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": ""}) - yield history, "", None, run_ids, gr.update(value="", visible=False) + yield history, "", None, run_ids, "", "" # thinking_box + source_box 초기화 collected_run_id: str | None = None - tts_text = "" # 순수 답변만 누적 (TTS용) - thinking_acc = "" # 사고 과정 누적 - thinking_active = False + tts_text = "" # 순수 답변만 누적 (TTS용) + thinking_acc = "" # 전체 누적 (완료 후 details용) + thinking_text = "" # __thinking 토큰만 (줄 감지용) + thinking_finalized = False + source_box_html = "" try: async for token, run_id in api_client.chat(message, user_id, show_thinking): @@ -115,40 +118,54 @@ async def respond(message, history, show_thinking, user_id, use_tts, run_ids): collected_run_id = run_id break - if isinstance(token, dict) and "__thinking" in token: - thinking_active = True - thinking_acc += token["__thinking"] - thinking_md = f"🤔 **사고 중...**\n\n{thinking_acc}▌" - yield history, "", None, run_ids, gr.update(value=thinking_md, visible=True) + # 즉시 상태 — thinking_acc에 누적 안 함 + if isinstance(token, dict) and "__status" in token: + if not thinking_acc: + yield history, "", None, run_ids, _status_html(token["__status"]), gr.update() continue - if thinking_active: - # 첫 답변 토큰 도착 — 사고 완료 표시 - thinking_active = False - yield history, "", None, run_ids, gr.update( - value=f"💭 **사고 완료**\n\n{thinking_acc}", visible=True - ) + # 사고 과정(LLM thinking) — 현재 줄만 live_html로 표시 + if isinstance(token, dict) and "__thinking" in token: + thinking_text += token["__thinking"] + thinking_acc += token["__thinking"] + yield history, "", None, run_ids, _live_html(_last_line(thinking_text)), gr.update() + continue + # 진행 로그(LangGraph, 검색 등) — 메시지 전체를 live_html로 표시 if isinstance(token, dict) and "__meta" in token: - display_token = token["__meta"] - else: - display_token = token - tts_text += display_token - history[-1]["content"] += display_token - yield history, "", None, run_ids, gr.update() + thinking_acc += token["__meta"] + live = token["__meta"].strip() + if live: + yield history, "", None, run_ids, _live_html(live), gr.update() + continue + + # RAG 출처 — 별도 source_box로 표시 + if isinstance(token, dict) and "__sources" in token: + source_box_html = _sources_html(token["__sources"]) + yield history, "", None, run_ids, gr.update(), source_box_html + continue + + # 첫 답변 토큰 도착 — 전체를 details로 전환 (접힌 상태) + if thinking_acc and not thinking_finalized: + thinking_finalized = True + yield history, "", None, run_ids, _thinking_html(thinking_acc), gr.update() + + tts_text += token + history[-1]["content"] += token + yield history, "", None, run_ids, gr.update(), gr.update() except Exception as e: history[-1]["content"] += f"\n\n[오류: {e}]" - yield history, "", None, run_ids, gr.update() + yield history, "", None, run_ids, gr.update(), gr.update() return run_ids.append(collected_run_id) if use_tts: audio_path = await tts_speak(tts_text) - yield history, "", audio_path, run_ids, gr.update() + yield history, "", audio_path, run_ids, gr.update(), gr.update() else: - yield history, "", None, run_ids, gr.update() + yield history, "", None, run_ids, gr.update(), gr.update() def handle_feedback(like_data: gr.LikeData, history, run_ids, user_id): @@ -222,22 +239,68 @@ def delete_doc(source): # ── UI 구성 ────────────────────────────────────────────────────── -_THINKING_CSS = """ -.thinking-box { - background: #f9f9f9; - border-left: 3px solid #bbb; - border-radius: 6px; - padding: 10px 14px; - margin-bottom: 6px; - max-height: 220px; - overflow-y: auto; - font-size: 0.85em; - color: #555; - white-space: pre-wrap; -} -""" +_BOX_STYLE = ( + "background:#f9f9f9;border-left:3px solid #bbb;border-radius:6px;" + "padding:8px 14px;margin-bottom:6px;" +) +_CONTENT_STYLE = ( + "margin-top:6px;white-space:pre-wrap;font-size:0.85em;" + "color:#555;max-height:160px;overflow-y:auto;" +) -with gr.Blocks(title="율봇", css=_THINKING_CSS) as demo: + +def _last_line(text: str) -> str: + """현재 진행 중인 마지막 비어있지 않은 줄 반환.""" + lines = [l for l in text.split("\n") if l.strip()] + return lines[-1] if lines else text.strip() + + +def _live_html(text: str) -> str: + """스트리밍 중 현재 줄만 보여주는 단순 div (details 미사용 → 닫힘 현상 없음).""" + return ( + f'