UI/UX D1: examples, image accordion, layout improvements, table select, button size

- app.py: add gr.Examples with 3 sample questions (D1-5)
- app.py: wrap image input in gr.Accordion(open=False) (D1-6)
- app.py: checkboxes left Column(scale=3), reset btn right Column(scale=1) (D1-7)
- app.py: msg_box lines=2, send_btn .send-btn CSS min-height 80px (D1-8)
- app.py: doc_table.select -> select_doc_row auto-fills delete_source (D1-9)
- app.py: ingest_btn scale=0 min_width=200 to avoid full-width stretch (D1-10)
- ROADMAP.md: mark all D1 items complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 14:28:21 +09:00
parent e08b43c785
commit c1a28bfdcc
2 changed files with 40 additions and 14 deletions
+6 -5
View File
@@ -331,11 +331,12 @@ youlbot-webui/
- [x] `결과` 박스 초기 `visible=False` — `ingest_files`에서 `gr.update(visible=True)` 반환 - [x] `결과` 박스 초기 `visible=False` — `ingest_files`에서 `gr.update(visible=True)` 반환
#### D1 #### D1
- [ ] 웰컴 메시지 또는 예시 질문 버블 추가 - [x] 웰컴 메시지 또는 예시 질문 버블 추가 — `gr.Examples` 3개 예시 질문
- [ ] 이미지 첨부 영역 `gr.Accordion`으로 접이식 변경 - [x] 이미지 첨부 영역 `gr.Accordion`으로 접이식 변경 — `open=False` 기본 접힘
- [ ] 하단 컨트롤 `gr.Row` 균등 배치 - [x] 하단 컨트롤 `gr.Row` 균등 배치 — 체크박스 좌측 `Column(scale=3)`, 초기화 버튼 우측 `Column(scale=1)`
- [ ] 문서 관리 탭 — 테이블 행 클릭 → 삭제 경로 자동 채움 - [x] 입력창 `lines=2` + 전송 버튼 높이 CSS 맞춤 — `.send-btn { min-height: 80px }`
- [ ] `문서 수집` 버튼 크기 적정화 - [x] 문서 관리 탭 — 테이블 행 클릭 → 삭제 경로 자동 채움 — `doc_table.select` + `select_doc_row`
- [x] `문서 수집` 버튼 크기 적정화 — `scale=0, min_width=200`
#### D2 #### D2
- [ ] 헤더 아이콘/로고 추가 - [ ] 헤더 아이콘/로고 추가
+34 -9
View File
@@ -189,6 +189,16 @@ async def list_docs():
return [[f"오류: {e}", ""]] return [[f"오류: {e}", ""]]
def select_doc_row(evt: gr.SelectData, doc_data):
row = evt.index[0]
try:
if hasattr(doc_data, "iloc"):
return str(doc_data.iloc[row, 1])
return str(doc_data[row][1])
except Exception:
return gr.update()
async def delete_doc(source): async def delete_doc(source):
if not source.strip(): if not source.strip():
return "삭제할 파일 경로를 입력하세요.", await list_docs() return "삭제할 파일 경로를 입력하세요.", await list_docs()
@@ -264,6 +274,7 @@ def _sources_html(sources: list) -> str:
_CUSTOM_CSS = """ _CUSTOM_CSS = """
footer { display: none !important; } footer { display: none !important; }
.send-btn { min-height: 80px !important; align-self: stretch !important; }
""" """
with gr.Blocks(title="율봇", css=_CUSTOM_CSS) as demo: with gr.Blocks(title="율봇", css=_CUSTOM_CSS) as demo:
@@ -288,19 +299,28 @@ with gr.Blocks(title="율봇", css=_CUSTOM_CSS) as demo:
msg_box = gr.Textbox( msg_box = gr.Textbox(
placeholder="질문을 입력하세요... (Enter로 전송)", placeholder="질문을 입력하세요... (Enter로 전송)",
show_label=False, show_label=False,
lines=2,
scale=5, scale=5,
autofocus=True, autofocus=True,
) )
send_btn = gr.Button("전송", variant="primary", scale=1) send_btn = gr.Button("전송", variant="primary", scale=1, elem_classes=["send-btn"])
with gr.Row(): gr.Examples(
examples=[
["육아휴직 급여 신청 방법을 알려주세요"],
["어린이집 입소 대기 기간은 얼마나 걸리나요?"],
["아이 의료비 세금 공제는 어떻게 하나요?"],
],
inputs=[msg_box],
label="💡 예시 질문",
)
with gr.Accordion("📷 이미지 첨부 (선택)", open=False):
image_input = gr.Image( image_input = gr.Image(
type="filepath", type="filepath",
label="📷 이미지 첨부 (선택)", show_label=False,
sources=["upload", "clipboard"], sources=["upload", "clipboard"],
height=160, height=160,
scale=1,
) )
gr.HTML("<div></div>", visible=False) # spacer
with gr.Accordion("🎤 음성으로 질문하기", open=False): with gr.Accordion("🎤 음성으로 질문하기", open=False):
with gr.Row(): with gr.Row():
@@ -313,9 +333,12 @@ with gr.Blocks(title="율봇", css=_CUSTOM_CSS) as demo:
transcribe_btn = gr.Button("음성 → 텍스트 변환", scale=1) transcribe_btn = gr.Button("음성 → 텍스트 변환", scale=1)
with gr.Row(): with gr.Row():
show_thinking = gr.Checkbox(label="사고 과정 표시", value=True) with gr.Column(scale=3):
use_tts = gr.Checkbox(label="음성으로 답변 읽기 (TTS)", value=False) with gr.Row():
reset_btn = gr.Button("대화 초기화", size="sm") show_thinking = gr.Checkbox(label="사고 과정 표시", value=True)
use_tts = gr.Checkbox(label="음성으로 답변 읽기 (TTS)", value=False)
with gr.Column(scale=1, min_width=120):
reset_btn = gr.Button("대화 초기화", size="sm")
tts_output = gr.Audio(label="음성 답변", autoplay=True, visible=False) tts_output = gr.Audio(label="음성 답변", autoplay=True, visible=False)
use_tts.change(lambda v: gr.Audio(visible=v), inputs=[use_tts], outputs=[tts_output]) use_tts.change(lambda v: gr.Audio(visible=v), inputs=[use_tts], outputs=[tts_output])
@@ -350,7 +373,8 @@ with gr.Blocks(title="율봇", css=_CUSTOM_CSS) as demo:
file_count="multiple", file_count="multiple",
label="파일 선택", label="파일 선택",
) )
ingest_btn = gr.Button("문서 수집", variant="primary") with gr.Row():
ingest_btn = gr.Button("문서 수집", variant="primary", scale=0, min_width=200)
ingest_status = gr.Textbox(label="결과", interactive=False, visible=False) ingest_status = gr.Textbox(label="결과", interactive=False, visible=False)
ingest_btn.click(ingest_files, inputs=[file_input], outputs=[ingest_status]) ingest_btn.click(ingest_files, inputs=[file_input], outputs=[ingest_status])
@@ -374,6 +398,7 @@ with gr.Blocks(title="율봇", css=_CUSTOM_CSS) as demo:
refresh_btn.click(list_docs, outputs=[doc_table]) refresh_btn.click(list_docs, outputs=[doc_table])
delete_btn.click(delete_doc, inputs=[delete_source], outputs=[delete_status, doc_table]) delete_btn.click(delete_doc, inputs=[delete_source], outputs=[delete_status, doc_table])
doc_table.select(select_doc_row, inputs=[doc_table], outputs=[delete_source])
demo.load(list_docs, outputs=[doc_table]) demo.load(list_docs, outputs=[doc_table])