Spaces:
Sleeping
Sleeping
import streamlit as st | |
import pandas as pd | |
import os | |
from src.SecondModule.module2 import SimilarQuestionGenerator | |
from src.ThirdModule.module3 import AnswerVerifier | |
import logging | |
from typing import Optional, Tuple | |
#from latex_formatter import LatexFormatter # LaTeX 포맷터 import | |
from pylatexenc.latex2text import LatexNodes2Text | |
import re | |
logging.basicConfig(level=logging.DEBUG) | |
# Streamlit 페이지 기본 설정 | |
st.set_page_config( | |
page_title="MisconcepTutor", | |
layout="wide", | |
initial_sidebar_state="expanded" | |
) | |
def load_answer_verifier(): | |
"""답안 검증 모델 로드""" | |
from src.ThirdModule.module3 import AnswerVerifier | |
return AnswerVerifier() | |
# 경로 설정 | |
base_path = os.path.dirname(os.path.abspath(__file__)) | |
data_path = os.path.join(base_path, 'Data') | |
misconception_csv_path = os.path.join(data_path, 'misconception_mapping.csv') | |
# 로깅 설정 | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# 세션 상태 초기화 - 가장 먼저 실행되도록 최상단에 배치 | |
if 'initialized' not in st.session_state: | |
st.session_state.initialized = True | |
st.session_state.wrong_questions = [] | |
st.session_state.misconceptions = [] | |
st.session_state.current_question_index = 0 | |
st.session_state.generated_questions = [] | |
st.session_state.current_step = 'initial' | |
st.session_state.selected_wrong_answer = None | |
st.session_state.questions = [] | |
logger.info("Session state initialized") | |
# 문제 생성기 초기화 | |
def load_question_generator(): | |
"""문제 생성 모델 로드""" | |
if not os.path.exists(misconception_csv_path): | |
st.error(f"CSV 파일이 존재하지 않습니다: {misconception_csv_path}") | |
raise FileNotFoundError(f"CSV 파일이 존재하지 않습니다: {misconception_csv_path}") | |
return SimilarQuestionGenerator(misconception_csv_path=misconception_csv_path) | |
# CSV 데이터 로드 함수 | |
def load_data(data_file = '/train.csv'): | |
try: | |
file_path = os.path.join(data_path, data_file.lstrip('/')) | |
df = pd.read_csv(file_path) | |
logger.info(f"Data loaded successfully from {file_path}") | |
return df | |
except FileNotFoundError: | |
st.error(f"파일을 찾을 수 없습니다: {data_file}") | |
logger.error(f"File not found: {data_file}") | |
return None | |
def start_quiz(): | |
"""퀴즈 시작 및 초기화""" | |
df = load_data() | |
if df is None or df.empty: | |
st.error("데이터를 불러올 수 없습니다. 데이터셋을 확인해주세요.") | |
return | |
st.session_state.questions = df.sample(n=10, random_state=42) | |
st.session_state.current_step = 'quiz' | |
st.session_state.current_question_index = 0 | |
st.session_state.wrong_questions = [] | |
st.session_state.misconceptions = [] | |
st.session_state.generated_questions = [] | |
logger.info("Quiz started") | |
def generate_similar_question(wrong_q, misconception_id, generator): | |
"""유사 문제 생성""" | |
logger.info(f"Generating similar question for misconception_id: {misconception_id}") | |
# 입력 데이터 유효성 검사 | |
if not isinstance(wrong_q, dict): | |
logger.error(f"Invalid wrong_q type: {type(wrong_q)}") | |
st.error("유사 문제 생성에 필요한 데이터 형식이 잘못되었습니다.") | |
return None | |
try: | |
# misconception_id가 없거나 NaN인 경우 다른 misconception 사용 | |
if pd.isna(misconception_id): | |
logger.info("Original misconception_id is NaN, trying to find alternative") | |
# 현재까지 나온 misconception들 중에서 선택 | |
available_misconceptions = [m for m in st.session_state.misconceptions if not pd.isna(m)] | |
if available_misconceptions: | |
# 가장 최근에 나온 misconception 선택 | |
misconception_id = available_misconceptions[-1] | |
logger.info(f"Using alternative misconception_id: {misconception_id}") | |
else: | |
# 기본 misconception ID 사용 (예: 가장 기본적인 misconception) | |
misconception_id = 2001 # 적절한 기본값으로 수정 필요 | |
logger.info(f"Using default misconception_id: {misconception_id}") | |
# 데이터 준비 (튜플 변환 방지) | |
input_data = { | |
'construct_name': str(wrong_q.get('ConstructName', '')), | |
'subject_name': str(wrong_q.get('SubjectName', '')), | |
'question_text': str(wrong_q.get('QuestionText', '')), | |
'correct_answer_text': str(wrong_q.get(f'Answer{wrong_q["CorrectAnswer"]}Text', '')), | |
'wrong_answer_text': str(wrong_q.get(f'Answer{st.session_state.selected_wrong_answer}Text', '')), | |
'misconception_id': int(misconception_id) | |
} | |
logger.info(f"Prepared input data: {input_data}") | |
with st.spinner("📝 유사 문제를 생성하고 있습니다..."): | |
# 유사 문제 생성 호출 | |
generated_q, _ = generator.generate_similar_question_with_text( | |
construct_name=input_data['construct_name'], | |
subject_name=input_data['subject_name'], | |
question_text=input_data['question_text'], | |
correct_answer_text=input_data['correct_answer_text'], | |
wrong_answer_text=input_data['wrong_answer_text'], | |
misconception_id=input_data['misconception_id'] | |
) | |
if generated_q: | |
verifier = load_answer_verifier() | |
with st.status("🤔 AI가 문제를 검토하고 있습니다..."): | |
st.write("답안의 정확성을 검증하고 있습니다...") | |
verified_answer = verifier.verify_answer( | |
question=generated_q.question, | |
choices=generated_q.choices | |
) | |
if verified_answer: | |
logger.info(f"Answer verified: {verified_answer}") | |
st.write("✅ 검증 완료!") | |
result = { | |
'question': generated_q.question, | |
'choices': generated_q.choices, | |
'correct': verified_answer, | |
'explanation': generated_q.explanation | |
} | |
st.session_state['current_similar_question_answer'] = verified_answer | |
return result | |
else: | |
logger.warning("Answer verification failed, using original answer") | |
st.write("⚠️ 검증에 실패했습니다. 원본 답안을 사용합니다.") | |
result = { | |
'question': generated_q.question, | |
'choices': generated_q.choices, | |
'correct': generated_q.correct_answer, | |
'explanation': generated_q.explanation | |
} | |
st.session_state['current_similar_question_answer'] = generated_q.correct_answer | |
return result | |
except Exception as e: | |
logger.error(f"Error in generate_similar_question: {str(e)}") | |
st.error(f"문제 생성 중 오류가 발생했습니다: {str(e)}") | |
return None | |
return None | |
# 수정 | |
def handle_answer(answer, current_q): | |
"""답변 처리""" | |
if answer != current_q['CorrectAnswer']: | |
wrong_q_dict = current_q.to_dict() | |
st.session_state.wrong_questions.append(wrong_q_dict) | |
st.session_state.selected_wrong_answer = answer | |
misconception_key = f'Misconception{answer}Id' | |
misconception_id = current_q.get(misconception_key) | |
st.session_state.misconceptions.append(misconception_id) | |
st.session_state.current_question_index += 1 | |
if st.session_state.current_question_index >= len(st.session_state.questions): | |
st.session_state.current_step = 'review' | |
else: | |
st.session_state.current_step = 'quiz' | |
# 수정 | |
def display_math_content(content): | |
""" | |
Display mathematical content with proper formatting. | |
Args: | |
content (str): The math content to display | |
""" | |
# Convert LaTeX to plain text for display | |
from pylatexenc.latex2text import LatexNodes2Text | |
# Clean and format the content | |
formatted_content = LatexNodes2Text().latex_to_text(content) | |
# Display in streamlit | |
st.markdown(f'<div class="math-container">{formatted_content}</div>', unsafe_allow_html=True) | |
# 추가 | |
def add_custom_css(): | |
st.markdown( | |
""" | |
<style> | |
.problem-header { | |
color: #FF6B6B; | |
font-size: 24px; | |
font-weight: bold; | |
margin-bottom: 20px; | |
} | |
.math-container { | |
background-color: #f9f9f9; | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
margin-bottom: 20px; | |
} | |
.options-container { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 20px; | |
} | |
.option { | |
background-color: white; | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
padding: 15px; | |
text-align: center; | |
font-size: 18px; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
} | |
.option:hover { | |
background-color: #f8f9fa; | |
} | |
</style> | |
""", | |
unsafe_allow_html=True | |
) | |
# 2210 | |
# def display_question(question, answers): | |
# """Display a math problem and its options""" | |
# st.markdown('<div class="problem-header">Problem:</div>', unsafe_allow_html=True) | |
# display_math_content(question) # 문제 렌더링 | |
# col1, col2 = st.columns(2) | |
# for i, (col, opt) in enumerate([(col1, 'A'), (col1, 'C'), (col2, 'B'), (col2, 'D')]): | |
# with col: | |
# if st.button(opt, key=f"btn_{opt}"): # 괄호 제거 | |
# handle_answer(opt, st.session_state.questions.iloc[st.session_state.current_question_index]) | |
# st.rerun() | |
# display_math_content(answers[opt]) # 보기 LaTeX 렌더링 | |
def display_question(question, answers): | |
"""Display a math problem and its options with LaTeX formatting""" | |
st.markdown('<div class="problem-header">Problem:</div>', unsafe_allow_html=True) | |
display_math_content(question) | |
col1, col2 = st.columns(2) | |
for i, (col, opt) in enumerate([(col1, 'A'), (col1, 'C'), (col2, 'B'), (col2, 'D')]): | |
with col: | |
answer_text = answers[opt] | |
if st.button(answer_text, key=f"btn_{opt}"): | |
handle_answer(opt, st.session_state.questions.iloc[st.session_state.current_question_index]) | |
st.rerun() | |
def main(): | |
"""메인 애플리케이션 로직""" | |
st.title("MisconcepTutor") | |
# Generator 초기화 | |
generator = load_question_generator() | |
# 초기 화면 | |
if st.session_state.current_step == 'initial': | |
st.write("#### 학습을 시작하겠습니다. 10개의 문제를 풀어볼까요?") | |
if st.button("학습 시작", key="start_quiz"): | |
start_quiz() | |
st.rerun() | |
# 퀴즈 화면 | |
elif st.session_state.current_step == 'quiz': | |
current_q = st.session_state.questions.iloc[st.session_state.current_question_index] | |
# 진행 상황 표시 | |
progress = st.session_state.current_question_index / 10 | |
st.progress(progress) | |
st.write(f"### 문제 {st.session_state.current_question_index + 1}/10") | |
# 문제 표시 | |
st.markdown("---") | |
question_row = current_q['QuestionText'] | |
question_text = LatexNodes2Text().latex_to_text(current_q['QuestionText']) | |
#st.write(current_q['QuestionText']) | |
#st.write(question_text) | |
answers ={ | |
'A': current_q['AnswerAText'], | |
'B': current_q['AnswerBText'], | |
'C': current_q['AnswerCText'], | |
'D': current_q['AnswerDText'] | |
} | |
display_question(question_text, answers) | |
# 복습 화면 | |
elif st.session_state.current_step == 'review': | |
st.write("### 학습 결과") | |
# 결과 통계 | |
col1, col2, col3 = st.columns(3) | |
col1.metric("총 문제 수", 10) | |
col2.metric("맞은 문제", 10 - len(st.session_state.wrong_questions)) | |
col3.metric("틀린 문제", len(st.session_state.wrong_questions)) | |
# 결과에 따른 메시지 표시 | |
if len(st.session_state.wrong_questions) == 0: | |
st.balloons() # 축하 효과 | |
st.success("🎉 축하합니다! 모든 문제를 맞추셨어요!") | |
st.markdown(""" | |
### 🏆 수학왕이십니다! | |
완벽한 점수를 받으셨네요! 수학적 개념을 정확하게 이해하고 계신 것 같습니다. | |
""") | |
elif len(st.session_state.wrong_questions) <= 3: | |
st.success("잘 하셨어요! 조금만 더 연습하면 완벽할 거예요!") | |
else: | |
st.info("천천히 개념을 복습해보아요. 연습하다 보면 늘어날 거예요!") | |
# 네비게이션 버튼 | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("🔄 새로운 문제 세트 시작하기", use_container_width=True): | |
start_quiz() | |
st.rerun() | |
with col2: | |
if st.button("🏠 처음으로 돌아가기", use_container_width=True): | |
st.session_state.clear() | |
st.rerun() | |
# 틀린 문제 분석 부분 | |
if st.session_state.wrong_questions: | |
st.write("### ✍️ 틀린 문제 분석") | |
tabs = st.tabs([f"📝 틀린 문제 #{i + 1}" for i in range(len(st.session_state.wrong_questions))]) | |
for i, (tab, (wrong_q, misconception_id)) in enumerate(zip( | |
tabs, | |
zip(st.session_state.wrong_questions, st.session_state.misconceptions) | |
)): | |
with tab: | |
st.write("**📋 문제:**") | |
#st.write(wrong_q['QuestionText']) | |
display_math_content(wrong_q['QuestionText']) # 문제 렌더링 | |
#st.write("**✅ 정답:**", wrong_q['CorrectAnswer']) | |
st.write("**✅ 정답:**") | |
display_math_content(wrong_q[f'Answer{wrong_q["CorrectAnswer"]}Text']) # 정답 렌더링 | |
st.write("---") | |
st.write("**🔍 관련된 Misconception:**") | |
if misconception_id and not pd.isna(misconception_id): | |
misconception_text = generator.get_misconception_text(misconception_id) | |
st.info(f"Misconception ID: {int(misconception_id)}\n\n{misconception_text}") | |
else: | |
st.info("Misconception 정보가 없습니다.") | |
if st.button(f"📚 유사 문제 풀기", key=f"retry_{i}"): | |
st.session_state[f"show_similar_question_{i}"] = True | |
st.session_state[f"similar_question_answered_{i}"] = False | |
st.rerun() | |
if st.session_state.get(f"show_similar_question_{i}", False): | |
st.divider() | |
new_question = generate_similar_question(wrong_q, misconception_id, generator) | |
if new_question: | |
st.write("### 🎯 유사 문제") | |
display_math_content(new_question['question']) # 함수 교체 | |
# 답변 상태 확인 | |
answered = st.session_state.get(f"similar_question_answered_{i}", False) | |
# 보기 표시 | |
st.write("**보기:**") | |
col1, col2 = st.columns(2) | |
# 답변하지 않은 경우에만 버튼 활성화 | |
if not answered: | |
with col1: | |
for option in ['A', 'C']: | |
if st.button( | |
f"{option}) {LatexNodes2Text().latex_to_text(new_question['choices'][option])}", | |
key=f"similar_{option}_{i}" | |
): | |
st.session_state[f"similar_question_answered_{i}"] = True | |
st.session_state[f"selected_answer_{i}"] = option | |
correct_answer = st.session_state.get('current_similar_question_answer') | |
if option == correct_answer: | |
st.session_state[f"is_correct_{i}"] = True | |
else: | |
st.session_state[f"is_correct_{i}"] = False | |
st.rerun() | |
with col2: | |
for option in ['B', 'D']: | |
if st.button( | |
f"{option}) {LatexNodes2Text().latex_to_text(new_question['choices'][option])}", | |
key=f"similar_{option}_{i}" | |
): | |
st.session_state[f"similar_question_answered_{i}"] = True | |
st.session_state[f"selected_answer_{i}"] = option | |
correct_answer = st.session_state.get('current_similar_question_answer') | |
if option == correct_answer: | |
st.session_state[f"is_correct_{i}"] = True | |
else: | |
st.session_state[f"is_correct_{i}"] = False | |
st.rerun() | |
# 답변한 경우 결과 표시 | |
if answered: | |
is_correct = st.session_state.get(f"is_correct_{i}", False) | |
correct_answer = st.session_state.get('current_similar_question_answer') | |
if is_correct: | |
st.success("✅ 정답입니다!") | |
else: | |
st.error(f"❌ 틀렸습니다. 정답은 {correct_answer}입니다.") | |
# 해설 표시 | |
st.write("---") | |
st.write("**📝 해설:**", new_question['explanation']) | |
# 다시 풀기 버튼 | |
if st.button("🔄 다시 풀기", key=f"reset_{i}"): | |
st.session_state[f"similar_question_answered_{i}"] = False | |
st.session_state[f"selected_answer_{i}"] = None | |
st.session_state[f"is_correct_{i}"] = None | |
st.rerun() | |
# 문제 닫기 버튼 | |
if st.button("❌ 문제 닫기", key=f"close_{i}"): | |
st.session_state[f"show_similar_question_{i}"] = False | |
st.session_state[f"similar_question_answered_{i}"] = False | |
st.session_state[f"selected_answer_{i}"] = None | |
st.session_state[f"is_correct_{i}"] = None | |
st.rerun() | |
# 화면 아래 여백 추가 | |
st.markdown("<br>" * 5, unsafe_allow_html=True) # 5줄의 빈 줄 추가 | |
st.markdown(""" | |
<div style="height: 100px;"> | |
</div> | |
""", unsafe_allow_html=True) # 추가 여백 | |
else: | |
st.error("유사 문제를 생성할 수 없습니다.") | |
if st.button("❌ 닫기", key=f"close_error_{i}"): | |
st.session_state[f"show_similar_question_{i}"] = False | |
st.rerun() | |
# 화면 아래 여백 추가 | |
st.markdown("<br>" * 5, unsafe_allow_html=True) # 5줄의 빈 줄 추가 | |
st.markdown(""" | |
<div style="height: 100px;"> | |
</div> | |
""", unsafe_allow_html=True) # 추가 여백 | |
if __name__ == "__main__": | |
main() | |
# random_state 42에서 정답 | |
# D C A A C | |
# A B B B B |