Compare commits
7 Commits
0803479438
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ab437d5d2e | |||
| fb438864b1 | |||
| c1a28bfdcc | |||
| e08b43c785 | |||
| 20f385e2a0 | |||
| 4565980915 | |||
| b13e3dfdd7 |
+76
@@ -278,6 +278,82 @@ youlbot-webui/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## UI/UX 디자인 개선
|
||||||
|
|
||||||
|
> 실제 UI 스크린샷 분석(2026-06-02) 기반 — 우선순위 순 정렬
|
||||||
|
|
||||||
|
### D0 — 즉시 수정 (노출 결함)
|
||||||
|
|
||||||
|
| # | 위치 | 문제 | 개선 방안 |
|
||||||
|
|---|------|------|-----------|
|
||||||
|
| 1 | 대화 탭 · 입력창 | `"Textbox"` 레이블이 입력창 위에 그대로 노출 | `gr.Textbox(label="", show_label=False, ...)` 또는 `label=None` |
|
||||||
|
| 2 | 대화 탭 · 음성 영역 | `"마이크를 찾을 수 ..."` 오류 메시지가 항상 노출 | 마이크 미사용 시 해당 텍스트 숨김 처리, 또는 `visible=False` 기본값 |
|
||||||
|
| 3 | 전체 탭 · 푸터 | `"Gradio로 제작됨 🎉"` Gradio 브랜딩 노출 | `css="footer { display: none; }"` 추가 또는 `gr.Blocks(show_footer=False)` |
|
||||||
|
| 4 | 문서 등록 탭 · 결과 박스 | 업로드 전에도 빈 `결과` 박스가 항상 노출 | 초기 `visible=False`, 수집 완료 시에만 `visible=True` 반환 |
|
||||||
|
|
||||||
|
### D1 — 1주일 내 (레이아웃 개선)
|
||||||
|
|
||||||
|
| # | 위치 | 문제 | 개선 방안 |
|
||||||
|
|---|------|------|-----------|
|
||||||
|
| 5 | 대화 탭 · 채팅 영역 | 초기 화면이 빈 흰 공간으로 시작 — 어색한 첫 인상 | 웰컴 메시지 또는 예시 질문 버블(`gr.Examples`) 추가 |
|
||||||
|
| 6 | 대화 탭 · 이미지 첨부 | 이미지 업로드 영역이 항상 전체 크기로 펼쳐짐 | `gr.Accordion("이미지 첨부 (선택)", open=False)` 로 접이식 변경 |
|
||||||
|
| 7 | 대화 탭 · 하단 컨트롤 | `사고 과정 표시`, `TTS`, `대화 초기화` 배치가 불규칙 | `gr.Row`로 균등 3분할, `대화 초기화`는 오른쪽 정렬 |
|
||||||
|
| 8 | 대화 탭 · 입력 영역 | 텍스트 입력창과 `전송` 버튼이 시각적으로 분리됨 | 입력창 높이를 `lines=2`로 통일, 버튼 높이 CSS로 맞춤 |
|
||||||
|
| 9 | 문서 관리 탭 · 삭제 UX | 경로를 테이블에서 복사해 입력 필드에 붙여넣기 해야 함 | 테이블 행 클릭 → 입력 필드 자동 채움 (`select` 이벤트 활용) |
|
||||||
|
| 10 | 문서 등록 탭 · 버튼 | `문서 수집` 버튼이 전체 너비를 차지해 무게감 과도 | `scale=0` 또는 `min_width=200` 으로 적정 크기 조절 |
|
||||||
|
|
||||||
|
### D2 — 2주일 내 (시각 품질)
|
||||||
|
|
||||||
|
| # | 항목 | 설명 |
|
||||||
|
|---|------|------|
|
||||||
|
| 11 | 헤더 브랜딩 | 텍스트 전용 헤더 → 아이콘/로고 이미지 + 서브타이틀 레이아웃으로 개선 |
|
||||||
|
| 12 | 커스텀 테마 | Gradio 기본 보라색 → `gr.themes.Soft()` 또는 커스텀 `primary_hue` 색상 지정 |
|
||||||
|
| 13 | 사용자 선택 위치 | `사용자` 드롭다운이 채팅 위에 있어 흐름 방해 → 헤더 우측 또는 사이드바로 이동 |
|
||||||
|
| 14 | 채팅 버블 스타일 | Gradio 기본 스타일 → CSS로 사용자/봇 버블 배경색·radius 차별화 |
|
||||||
|
| 15 | 응답 로딩 표시 | 스트리밍 중 시각적 피드백 없음 → 입력 비활성화 + 스피너 CSS 추가 |
|
||||||
|
| 16 | 반응형 레이아웃 | 좁은 뷰포트에서 요소 겹침 → `gr.Column`/`gr.Row` 비율 재조정 |
|
||||||
|
|
||||||
|
### D3 — 선택 사항 (장기)
|
||||||
|
|
||||||
|
| # | 항목 | 설명 |
|
||||||
|
|---|------|------|
|
||||||
|
| 17 | 다크 모드 | `gr.themes.Base()` + CSS 변수로 다크/라이트 토글 지원 |
|
||||||
|
| 18 | 채팅 내보내기 | 대화 내용을 `.txt`/`.md`로 다운로드하는 버튼 추가 |
|
||||||
|
| 19 | 접근성 | `aria-label` 속성 추가, 키보드 포커스 표시 개선 |
|
||||||
|
| 20 | 온보딩 투어 | 첫 방문 사용자 대상 단계별 기능 안내 (JS 오버레이) |
|
||||||
|
|
||||||
|
### D 체크리스트
|
||||||
|
|
||||||
|
#### D0
|
||||||
|
- [x] `gr.Textbox(show_label=False)` — `"Textbox"` 레이블 제거
|
||||||
|
- [x] 음성 오류 메시지 기본 숨김 처리 — `gr.Accordion("🎤 음성으로 질문하기", open=False)` 로 기본 접힘
|
||||||
|
- [x] Gradio 푸터 CSS 숨김 — `footer { display: none !important; }` 추가
|
||||||
|
- [x] `결과` 박스 초기 `visible=False` — `ingest_files`에서 `gr.update(visible=True)` 반환
|
||||||
|
|
||||||
|
#### D1
|
||||||
|
- [x] 웰컴 메시지 또는 예시 질문 버블 추가 — `gr.Examples` 3개 예시 질문
|
||||||
|
- [x] 이미지 첨부 영역 `gr.Accordion`으로 접이식 변경 — `open=False` 기본 접힘
|
||||||
|
- [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
|
||||||
|
- [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
|
||||||
|
- [x] 다크 모드 토글 — 헤더 🌙/☀️ 버튼 + `localStorage` 기억 + 시스템 설정 자동 감지
|
||||||
|
- [x] 채팅 내보내기 — `export_chat` 함수, `.md` 파일 다운로드, `gr.File` 출력
|
||||||
|
- [x] 접근성 개선 — `aria-label`/`role`/`aria-live` JS 주입, `:focus-visible` 파란 테두리
|
||||||
|
- [x] 온보딩 투어 — 첫 방문 모달 (`localStorage` 체크, "시작하기" 버튼으로 닫기)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 진행 체크리스트
|
## 진행 체크리스트
|
||||||
|
|
||||||
### P0
|
### P0
|
||||||
|
|||||||
@@ -164,11 +164,29 @@ async def reset_chat(user_id):
|
|||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
|
|
||||||
|
async def export_chat(history):
|
||||||
|
if not history:
|
||||||
|
return gr.update(visible=False)
|
||||||
|
from datetime import datetime
|
||||||
|
import tempfile
|
||||||
|
lines = [f"# 율봇 대화 내보내기\n_내보낸 시각: {datetime.now().strftime('%Y-%m-%d %H:%M')}_\n\n"]
|
||||||
|
for msg in history:
|
||||||
|
role = "👤 사용자" if msg["role"] == "user" else "🤖 율봇"
|
||||||
|
content = str(msg.get("content") or "")
|
||||||
|
lines.append(f"### {role}\n\n{content}\n\n---\n\n")
|
||||||
|
tmp = tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".md", delete=False, encoding="utf-8", prefix="youlbot_chat_"
|
||||||
|
)
|
||||||
|
tmp.write("".join(lines))
|
||||||
|
tmp.close()
|
||||||
|
return gr.update(value=tmp.name, visible=True)
|
||||||
|
|
||||||
|
|
||||||
# ── 문서 관리 ─────────────────────────────────────────────────────
|
# ── 문서 관리 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def ingest_files(files):
|
async def ingest_files(files):
|
||||||
if not files:
|
if not files:
|
||||||
return "파일을 선택해주세요."
|
return gr.update(value="파일을 선택해주세요.", visible=True)
|
||||||
paths = [f if isinstance(f, str) else f.name for f in files]
|
paths = [f if isinstance(f, str) else f.name for f in files]
|
||||||
results = []
|
results = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
@@ -178,7 +196,7 @@ async def ingest_files(files):
|
|||||||
results.append(f"{name} → {result.get('chunks', '?')}개 청크")
|
results.append(f"{name} → {result.get('chunks', '?')}개 청크")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append(f"{os.path.basename(path)} 오류: {e}")
|
results.append(f"{os.path.basename(path)} 오류: {e}")
|
||||||
return "\n".join(results)
|
return gr.update(value="\n".join(results), visible=True)
|
||||||
|
|
||||||
|
|
||||||
async def list_docs():
|
async def list_docs():
|
||||||
@@ -189,6 +207,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()
|
||||||
@@ -221,7 +249,7 @@ def _live_html(text: str) -> str:
|
|||||||
"""스트리밍 중 현재 줄만 보여주는 단순 div (details 미사용 → 닫힘 현상 없음)."""
|
"""스트리밍 중 현재 줄만 보여주는 단순 div (details 미사용 → 닫힘 현상 없음)."""
|
||||||
return (
|
return (
|
||||||
f'<div style="{_BOX_STYLE}">'
|
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 style="{_CONTENT_STYLE}">{_html.escape(text)} ▌</div>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
)
|
)
|
||||||
@@ -262,55 +290,214 @@ def _sources_html(sources: list) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
with gr.Blocks(title="율봇") as demo:
|
_JS = """
|
||||||
gr.Markdown("# 율봇\n육아·금융 전문 AI 상담 도우미")
|
() => {
|
||||||
|
const htmlEl = document.documentElement;
|
||||||
|
|
||||||
|
// D3-17: 다크 모드 토글
|
||||||
|
window.toggleDarkMode = function() {
|
||||||
|
htmlEl.classList.toggle('dark');
|
||||||
|
const isDark = htmlEl.classList.contains('dark');
|
||||||
|
localStorage.setItem('youlbot_theme', isDark ? 'dark' : 'light');
|
||||||
|
const btn = document.getElementById('dark-mode-btn');
|
||||||
|
if (btn) btn.textContent = isDark ? '☀️' : '🌙';
|
||||||
|
};
|
||||||
|
const saved = localStorage.getItem('youlbot_theme');
|
||||||
|
if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
htmlEl.classList.add('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// D3-19: aria 레이블 설정
|
||||||
|
function addAriaLabels() {
|
||||||
|
const ta = document.querySelector('textarea[placeholder*="질문"]');
|
||||||
|
if (ta) ta.setAttribute('aria-label', '질문 입력창 (Enter 키로 전송)');
|
||||||
|
document.querySelectorAll('.send-btn').forEach(b => b.setAttribute('aria-label', '메시지 전송'));
|
||||||
|
const cb = document.getElementById('main-chatbot');
|
||||||
|
if (cb) { cb.setAttribute('role', 'log'); cb.setAttribute('aria-live', 'polite'); cb.setAttribute('aria-label', '대화 내용'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// D3-20: 첫 방문 온보딩 모달
|
||||||
|
function showOnboarding() {
|
||||||
|
if (localStorage.getItem('youlbot_onboarded')) return;
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.id = 'youlbot-onboarding';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div style="position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px;">
|
||||||
|
<div style="background:#fff;border-radius:20px;padding:36px 32px;max-width:400px;width:100%;box-shadow:0 24px 64px rgba(0,0,0,.25);">
|
||||||
|
<div style="font-size:2.4rem;text-align:center;margin-bottom:12px;">🤖</div>
|
||||||
|
<h2 style="margin:0 0 6px;font-size:1.3rem;font-weight:700;text-align:center;color:#1e293b;">율봇에 오신 것을 환영합니다!</h2>
|
||||||
|
<p style="margin:0 0 20px;text-align:center;color:#64748b;font-size:.875rem;">육아·금융 전문 AI 상담 도우미</p>
|
||||||
|
<ul style="list-style:none;padding:0;margin:0 0 24px;display:flex;flex-direction:column;gap:10px;">
|
||||||
|
<li style="display:flex;align-items:center;gap:10px;font-size:.9rem;color:#374151;"><span>💬</span> 아래 입력창에 질문을 입력하세요</li>
|
||||||
|
<li style="display:flex;align-items:center;gap:10px;font-size:.9rem;color:#374151;"><span>💡</span> 예시 질문 클릭으로 빠르게 시작</li>
|
||||||
|
<li style="display:flex;align-items:center;gap:10px;font-size:.9rem;color:#374151;"><span>📄</span> 문서 등록 탭에서 RAG 문서 추가</li>
|
||||||
|
<li style="display:flex;align-items:center;gap:10px;font-size:.9rem;color:#374151;"><span>🎤</span> 음성으로도 질문 가능</li>
|
||||||
|
</ul>
|
||||||
|
<button onclick="window.closeOnboarding()" style="width:100%;padding:13px;background:#3b82f6;color:#fff;border:none;border-radius:10px;font-size:.95rem;font-weight:600;cursor:pointer;" onmouseover="this.style.background='#2563eb'" onmouseout="this.style.background='#3b82f6'">시작하기</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
window.closeOnboarding = function() {
|
||||||
|
const m = document.getElementById('youlbot-onboarding');
|
||||||
|
if (m) m.remove();
|
||||||
|
localStorage.setItem('youlbot_onboarded', '1');
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
addAriaLabels();
|
||||||
|
showOnboarding();
|
||||||
|
const btn = document.getElementById('dark-mode-btn');
|
||||||
|
if (btn && htmlEl.classList.contains('dark')) btn.textContent = '☀️';
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
_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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 접근성: 포커스 표시 강화 (D3-19) */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 3px solid #3b82f6 !important;
|
||||||
|
outline-offset: 2px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 다크 모드 커스텀 요소 오버라이드 (D3-17) */
|
||||||
|
.dark .message-wrap .user { background: #1e3a5f !important; border-color: #2563eb !important; }
|
||||||
|
.dark .message-wrap .bot { background: #1e293b !important; border-color: #334155 !important; }
|
||||||
|
.dark .app-header { border-color: #334155 !important; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
_THEME = gr.themes.Soft(
|
||||||
|
primary_hue="blue",
|
||||||
|
secondary_hue="indigo",
|
||||||
|
neutral_hue="slate",
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Blocks(title="율봇", css=_CUSTOM_CSS, theme=_THEME, js=_JS) 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>
|
||||||
|
""")
|
||||||
|
gr.HTML("""
|
||||||
|
<div style="display:flex;justify-content:flex-end;align-items:center;height:100%;">
|
||||||
|
<button id="dark-mode-btn" onclick="toggleDarkMode()" title="다크/라이트 모드 전환"
|
||||||
|
style="background:none;border:1.5px solid #e2e8f0;border-radius:8px;cursor:pointer;
|
||||||
|
font-size:1.2rem;padding:6px 10px;line-height:1;color:#64748b;transition:all .15s;"
|
||||||
|
onmouseover="this.style.borderColor='#94a3b8'"
|
||||||
|
onmouseout="this.style.borderColor='#e2e8f0'">🌙</button>
|
||||||
|
</div>
|
||||||
|
""", scale=0, min_width=60)
|
||||||
|
user_selector = gr.Dropdown(
|
||||||
|
choices=USER_LABELS,
|
||||||
|
value=DEFAULT_USER,
|
||||||
|
label="사용자",
|
||||||
|
scale=0,
|
||||||
|
min_width=160,
|
||||||
|
)
|
||||||
|
|
||||||
user_state = gr.State(DEFAULT_USER)
|
user_state = gr.State(DEFAULT_USER)
|
||||||
run_ids_state = gr.State([])
|
run_ids_state = gr.State([])
|
||||||
|
|
||||||
with gr.Tab("대화"):
|
with gr.Tab("대화"):
|
||||||
with gr.Row():
|
|
||||||
user_selector = gr.Dropdown(
|
|
||||||
choices=USER_LABELS,
|
|
||||||
value=DEFAULT_USER,
|
|
||||||
label="사용자",
|
|
||||||
scale=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
thinking_box = gr.HTML(value="")
|
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="")
|
source_box = gr.HTML(value="")
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
msg_box = gr.Textbox(
|
msg_box = gr.Textbox(
|
||||||
placeholder="질문을 입력하세요... (Enter로 전송)",
|
placeholder="질문을 입력하세요... (Enter로 전송)",
|
||||||
label="",
|
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.Row():
|
||||||
|
audio_input = gr.Audio(
|
||||||
|
sources=["microphone"],
|
||||||
|
type="filepath",
|
||||||
|
show_label=False,
|
||||||
|
scale=4,
|
||||||
|
)
|
||||||
|
transcribe_btn = gr.Button("음성 → 텍스트 변환", scale=1)
|
||||||
|
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
audio_input = gr.Audio(
|
with gr.Column(scale=3):
|
||||||
sources=["microphone"],
|
with gr.Row():
|
||||||
type="filepath",
|
show_thinking = gr.Checkbox(label="사고 과정 표시", value=True)
|
||||||
label="음성으로 질문하기",
|
use_tts = gr.Checkbox(label="음성으로 답변 읽기 (TTS)", value=False)
|
||||||
scale=4,
|
with gr.Column(scale=2, min_width=240):
|
||||||
)
|
with gr.Row():
|
||||||
transcribe_btn = gr.Button("음성 → 텍스트 변환", scale=1)
|
export_btn = gr.Button("💾 내보내기", size="sm", min_width=100)
|
||||||
|
reset_btn = gr.Button("대화 초기화", size="sm", min_width=100)
|
||||||
|
|
||||||
with gr.Row():
|
export_file = gr.File(label="내보내기 파일", visible=False)
|
||||||
show_thinking = gr.Checkbox(label="사고 과정 표시", value=True)
|
|
||||||
use_tts = gr.Checkbox(label="음성으로 답변 읽기 (TTS)", value=False)
|
|
||||||
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])
|
||||||
@@ -331,6 +518,7 @@ with gr.Blocks(title="율봇") as demo:
|
|||||||
send_btn.click(respond, inputs=_respond_inputs, outputs=_respond_outputs)
|
send_btn.click(respond, inputs=_respond_inputs, outputs=_respond_outputs)
|
||||||
msg_box.submit(respond, inputs=_respond_inputs, outputs=_respond_outputs)
|
msg_box.submit(respond, inputs=_respond_inputs, outputs=_respond_outputs)
|
||||||
reset_btn.click(reset_chat, inputs=[user_state], outputs=[chatbot, run_ids_state])
|
reset_btn.click(reset_chat, inputs=[user_state], outputs=[chatbot, run_ids_state])
|
||||||
|
export_btn.click(export_chat, inputs=[chatbot], outputs=[export_file])
|
||||||
|
|
||||||
chatbot.like(
|
chatbot.like(
|
||||||
handle_feedback,
|
handle_feedback,
|
||||||
@@ -345,8 +533,9 @@ with gr.Blocks(title="율봇") as demo:
|
|||||||
file_count="multiple",
|
file_count="multiple",
|
||||||
label="파일 선택",
|
label="파일 선택",
|
||||||
)
|
)
|
||||||
ingest_btn = gr.Button("문서 수집", variant="primary")
|
with gr.Row():
|
||||||
ingest_status = gr.Textbox(label="결과", interactive=False)
|
ingest_btn = gr.Button("문서 수집", variant="primary", scale=0, min_width=200)
|
||||||
|
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])
|
||||||
|
|
||||||
with gr.Tab("문서 관리"):
|
with gr.Tab("문서 관리"):
|
||||||
@@ -369,6 +558,7 @@ with gr.Blocks(title="율봇") 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])
|
||||||
|
|
||||||
|
|
||||||
@@ -376,5 +566,4 @@ if __name__ == "__main__":
|
|||||||
demo.launch(
|
demo.launch(
|
||||||
server_name=container.config().server_host,
|
server_name=container.config().server_host,
|
||||||
server_port=container.config().server_port,
|
server_port=container.config().server_port,
|
||||||
theme=gr.themes.Soft(),
|
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user