kwmin_probin / ui /components.py
cksleigen's picture
add files
c2f0e66
# ui/components.py
"""PROBIN UI ์ปดํฌ๋„ŒํŠธ"""
import streamlit as st
from typing import List, Dict
def render_header():
"""ํ—ค๋” ๋ Œ๋”๋ง (PROBIN ๋ฒ„์ „)"""
st.markdown(
"""
<div style="text-align: center; padding: 2rem 0;">
<h1 style="color: #2196F3; font-size: 3rem;">๐Ÿง  PROBIN</h1>
<p style="font-size: 1.2rem; color: #666;">Intelligent Document Analysis System</p>
<p style="font-size: 0.9rem; color: #999;">์ •ํ™•๋„ ์šฐ์„  RAG ์‹œ์Šคํ…œ</p>
</div>
""",
unsafe_allow_html=True
)
def render_sources_with_relevance(sources: List[Dict], message_idx: int, move_to_page_callback):
"""
์ถœ์ฒ˜ ๋ Œ๋”๋ง (๊ฐ€์žฅ ๊ด€๋ จ๋„ ๋†’์€ ๊ฒƒ ์šฐ์„ )
Args:
sources: ์ถœ์ฒ˜ ๋ฆฌ์ŠคํŠธ (์ด๋ฏธ ๊ด€๋ จ๋„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ)
message_idx: ๋ฉ”์‹œ์ง€ ์ธ๋ฑ์Šค (ํ‚ค ์ค‘๋ณต ๋ฐฉ์ง€์šฉ)
move_to_page_callback: ํŽ˜์ด์ง€ ์ด๋™ ์ฝœ๋ฐฑ ํ•จ์ˆ˜
๊ตฌ์กฐ:
๐ŸŽฏ ํ•ต์‹ฌ ๊ทผ๊ฑฐ (1๊ฐœ) - sources[0]: ๊ฐ€์žฅ ๊ด€๋ จ๋„ ๋†’์€ ์ถœ์ฒ˜
๐Ÿ“š ์ถ”๊ฐ€ ์ฐธ๊ณ  ๋ฌธ์„œ (๋‚˜๋จธ์ง€) - Expander ๋‚ด๋ถ€
"""
if not sources:
return
# ์ฒซ ๋ฒˆ์งธ ์ถœ์ฒ˜ = ๊ฐ€์žฅ ๊ด€๋ จ๋„ ๋†’์Œ
st.markdown("---")
st.markdown("**๐ŸŽฏ ํ•ต์‹ฌ ๊ทผ๊ฑฐ:**")
primary_source = sources[0]
if st.button(
f"๐Ÿ“ ํŽ˜์ด์ง€ {primary_source['page_num']} ํ™•์ธ",
key=f"primary_{message_idx}",
type="primary",
use_container_width=True
):
move_to_page_callback(primary_source['page_num'], primary_source['text'])
# ๋ฏธ๋ฆฌ๋ณด๊ธฐ
st.caption(f"๐Ÿ’ฌ \"{primary_source['text'][:100]}...\"")
# ์ถ”๊ฐ€ ์ฐธ๊ณ  ๋ฌธ์„œ (๋‚˜๋จธ์ง€)
if len(sources) > 1:
additional_sources = sources[1:]
with st.expander(f"๐Ÿ“š ์ถ”๊ฐ€ ์ฐธ๊ณ  ๋ฌธ์„œ ({min(2, len(additional_sources))}๊ฐœ)"):
cols = st.columns(2)
for i, src in enumerate(additional_sources[:2]): # ์ตœ๋Œ€ 2๊ฐœ๋งŒ
with cols[i]:
if st.button(
f"p.{src['page_num']}",
key=f"additional_{message_idx}_{i}",
use_container_width=True
):
move_to_page_callback(src['page_num'], src['text'])
st.caption(f"\"{src['text'][:60]}...\"")
def render_answer(answer: str, sources: List[Dict]):
"""๋‹ต๋ณ€ ๋ฐ ์ถœ์ฒ˜ ๋ Œ๋”๋ง (Legacy - ์‚ฌ์šฉ ์•ˆ ํ•จ)"""
# ๋‹ต๋ณ€
st.markdown("### ๐Ÿ’ก ๋‹ต๋ณ€")
st.markdown(
f"""
<div class="answer-box">
{answer}
</div>
""",
unsafe_allow_html=True
)
# ์ถœ์ฒ˜
if sources:
st.markdown("### ๐Ÿ“„ ์ถœ์ฒ˜")
for i, source in enumerate(sources, 1):
with st.expander(f"์ถœ์ฒ˜ {i} - ํŽ˜์ด์ง€ {source['page_num']}"):
st.text(source['text'])
def render_file_uploader():
"""ํŒŒ์ผ ์—…๋กœ๋” ๋ Œ๋”๋ง"""
st.markdown("### ๐Ÿ“ค PDF ์—…๋กœ๋“œ")
uploaded_file = st.file_uploader(
"RFP PDF ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์„ธ์š”",
type=["pdf"],
help="PDF ํ˜•์‹์˜ RFP ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•˜์„ธ์š”"
)
return uploaded_file
def render_query_input():
"""์งˆ๋ฌธ ์ž…๋ ฅ ๋ Œ๋”๋ง (Deprecated: st.chat_input ์‚ฌ์šฉ ๊ถŒ์žฅ)"""
st.markdown("### ๐Ÿ’ฌ ์งˆ๋ฌธํ•˜๊ธฐ")
query = st.text_input(
"์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”",
placeholder="์˜ˆ: ์ด ํ”„๋กœ์ ํŠธ์˜ ์˜ˆ์‚ฐ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
help="RFP ๋ฌธ์„œ์— ๋Œ€ํ•ด ์งˆ๋ฌธํ•˜์„ธ์š”"
)
return query
def render_chat_history(messages: list):
"""์ฑ„ํŒ… ํžˆ์Šคํ† ๋ฆฌ ๋ Œ๋”๋ง (ํ˜„๋Œ€์  ๋ฐฉ์‹)"""
for message in messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# ์ถœ์ฒ˜ ํ‘œ์‹œ
if message["role"] == "assistant" and "sources" in message:
with st.expander("๐Ÿ“š ์ถœ์ฒ˜ ๋ณด๊ธฐ"):
for i, src in enumerate(message["sources"], 1):
st.markdown(f"**์ถœ์ฒ˜ {i} - [ํŽ˜์ด์ง€ {src['page_num']}]**")
st.caption(src["text"])
def render_sidebar():
"""์‚ฌ์ด๋“œ๋ฐ” ๋ Œ๋”๋ง"""
with st.sidebar:
st.markdown("## โš™๏ธ ์„ค์ •")
# ๊ฒ€์ƒ‰ ์„ค์ •
st.markdown("### ๐Ÿ” ๊ฒ€์ƒ‰ ์„ค์ •")
top_k = st.slider("๊ฒ€์ƒ‰ํ•  ์ฒญํฌ ์ˆ˜", 5, 20, 10)
# ์ฒญํ‚น ์„ค์ •
st.markdown("### โœ‚๏ธ ์ฒญํ‚น ์„ค์ •")
chunk_size = st.number_input("์ฒญํฌ ํฌ๊ธฐ", 400, 1200, 800, step=100)
chunk_overlap = st.number_input("์˜ค๋ฒ„๋žฉ ํฌ๊ธฐ", 50, 300, 150, step=50)
st.markdown("---")
# ์ •๋ณด
st.markdown("### โ„น๏ธ ์ •๋ณด")
st.info(
"""
**PROBIN v2.0**
- PDF ์—…๋กœ๋“œ โœ…
- ์งˆ๋ฌธ-๋‹ต๋ณ€ โœ…
- ์ถœ์ฒ˜ ํ‘œ์‹œ โœ…
- ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ โœ…
- ํ•˜์ด๋ผ์ดํŠธ โœ…
"""
)
# ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ
if st.button("๐Ÿ—‘๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”", type="secondary"):
return True, top_k, chunk_size, chunk_overlap
return False, top_k, chunk_size, chunk_overlap
def render_processing_status(status: str):
"""์ฒ˜๋ฆฌ ์ƒํƒœ ๋ Œ๋”๋ง"""
status_icons = {
"uploading": "๐Ÿ“ค",
"extracting": "๐Ÿ“„",
"chunking": "โœ‚๏ธ",
"embedding": "๐Ÿ”ข",
"storing": "๐Ÿ’พ",
"complete": "โœ…"
}
icon = status_icons.get(status, "โณ")
st.info(f"{icon} {status}")
def render_welcome_message():
"""์›ฐ์ปด ๋ฉ”์‹œ์ง€ (์‚ฌ์šฉ ์•ˆ๋‚ด)"""
st.markdown("""
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px; text-align: center;">
<h2 style="margin: 0 0 10px 0;">๐Ÿง  PROBIN์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!</h2>
<p style="margin: 0; opacity: 0.9;">Intelligent Document Analysis System</p>
</div>
<div style="background-color: #f0f7ff; border-left: 4px solid #2196F3; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
<h4 style="margin-top: 0; color: #1976D2;">๐Ÿ“– ์ด์šฉ ๋ฐฉ๋ฒ•</h4>
<ol style="line-height: 1.8; margin: 0;">
<li><strong>PDF ์—…๋กœ๋“œ:</strong> ์™ผ์ชฝ ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•˜์„ธ์š”</li>
<li><strong>๋ฌธ์„œ ์ฒ˜๋ฆฌ:</strong> 30์ดˆ~1๋ถ„ ์ •๋„ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค</li>
<li><strong>์งˆ๋ฌธ ์ž…๋ ฅ:</strong> ์ฑ„ํŒ…์ฐฝ์— ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”</li>
<li><strong>๊ทผ๊ฑฐ ํ™•์ธ:</strong> <span style="background-color: rgba(255,255,0,0.5); padding: 2px 6px; border-radius: 3px;">๋…ธ๋ž€์ƒ‰ ํ•˜์ด๋ผ์ดํŠธ</span>๋กœ ๊ทผ๊ฑฐ๋ฅผ ํ™•์ธํ•˜์„ธ์š”</li>
</ol>
</div>
<div style="text-align: center; margin-top: 20px;">
<span style="display: inline-block; margin: 5px; padding: 8px 16px; background-color: #e8f5e9; border-radius: 20px; font-size: 0.9rem;">๐Ÿ“บ Split View</span>
<span style="display: inline-block; margin: 5px; padding: 8px 16px; background-color: #fff3e0; border-radius: 20px; font-size: 0.9rem;">๐Ÿ–๏ธ Highlighting</span>
<span style="display: inline-block; margin: 5px; padding: 8px 16px; background-color: #f3e5f5; border-radius: 20px; font-size: 0.9rem;">๐ŸŽฏ High Accuracy</span>
</div>
""", unsafe_allow_html=True)