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 logging.basicConfig(level=logging.DEBUG) # Streamlit 페이지 기본 설정 st.set_page_config( page_title="MisconcepTutor", layout="wide", initial_sidebar_state="expanded" ) @st.cache_resource 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") # 문제 생성기 초기화 @st.cache_resource 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 데이터 로드 함수 @st.cache_data 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 >= 10: st.session_state.current_step = 'review' def format_question(question: str) -> str: """문제 텍스트를 LaTeX 형식으로 변환""" # 색상이 있는 텍스트 처리 colored_text_pattern = r'\\textcolor{([^}]+)}{([^}]+)}' def process_colored_text(match): color = match.group(1) text = match.group(2) return f'\\textcolor{{{color}}}{{{format_latex_expression(text)}}}' question = re.sub(colored_text_pattern, process_colored_text, question) return format_latex_expression(question) def format_answer_choice(choice: str) -> str: """선택지 텍스트를 LaTeX 형식으로 변환""" # 달러 기호가 포함된 선택지 특별 처리 if '$' in choice: # 달러 기호를 LaTeX 명령어로 변환 choice = choice.replace('$', '\\$') return format_latex_expression(choice) def display_math_content(content: str): """수학 내용을 화면에 표시""" formatted_content = format_latex_expression(content) # LaTeX 렌더링을 위한 추가 설정 st.markdown(formatted_content, unsafe_allow_html=True) def format_latex_expression(text: str) -> str: """복잡한 LaTeX 수식과 특수 기호를 처리하는 함수""" import re # LaTeX 특수 명령어 매핑 latex_commands = { r'\left': r'\\left', r'\right': r'\\right', r'\bigcirc': r'\\bigcirc', r'\square': r'\\square', r'\quad': r'\\quad' } # 1. 이미 존재하는 LaTeX 수식 보존 latex_parts = [] def save_latex(match): latex_parts.append(match.group(0)) return f"LATEX_{len(latex_parts)-1}_PLACEHOLDER" text = re.sub(r'\$\$.*?\$\$', save_latex, text) # 2. 특수 명령어 처리 for cmd, latex_cmd in latex_commands.items(): text = text.replace(cmd, latex_cmd) # 3. 붙어있는 단어 분리 text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text) text = re.sub(r'([A-Za-z])(\d)', r'\1 \2', text) text = re.sub(r'(\d)([A-Za-z])', r'\1 \2', text) # 4. 문장 분석 sentences = text.split('$$') formatted_sentences = [] for i, sentence in enumerate(sentences): if i % 2 == 0: # 일반 텍스트 # 수식이 아닌 부분의 특수 문자 처리 for cmd, latex_cmd in latex_commands.items(): if cmd in sentence: sentence = f"$${sentence}$$" break formatted_sentences.append(sentence) else: # 수식 formatted_sentences.append(f"$${sentence}$$") text = ''.join(formatted_sentences) # 5. LaTeX 수식 복원 for i, latex in enumerate(latex_parts): text = text.replace(f"LATEX_{i}_PLACEHOLDER", latex) # 6. 마지막 정리 text = text.replace('\\\\', '\\') # 중복된 백슬래시 제거 text = re.sub(r'\s+', ' ', text) # 여러 개의 공백을 하나로 return text 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("---") #display_math_question(current_q['QuestionText']) display_math_content(current_q['QuestionText']) # display_math_question 대신 display_math_content 사용 #st.write(current_q['QuestionText']) # # 보기 표시 # col1, col2 = st.columns(2) # with col1: # if st.button(f"A) {current_q['AnswerAText']}", key="A"): # handle_answer('A', current_q) # st.rerun() # if st.button(f"C) {current_q['AnswerCText']}", key="C"): # handle_answer('C', current_q) # st.rerun() # with col2: # if st.button(f"B) {current_q['AnswerBText']}", key="B"): # handle_answer('B', current_q) # st.rerun() # if st.button(f"D) {current_q['AnswerDText']}", key="D"): # handle_answer('D', current_q) # st.rerun() # 보기 표시 col1, col2 = st.columns(2) with col1: if st.button(format_latex_expression(f"A) {current_q['AnswerAText']}"), key="A"): handle_answer('A', current_q) st.rerun() if st.button(format_latex_expression(f"C) {current_q['AnswerCText']}"), key="C"): handle_answer('C', current_q) st.rerun() with col2: if st.button(format_latex_expression(f"B) {current_q['AnswerBText']}"), key="B"): handle_answer('B', current_q) st.rerun() if st.button(format_latex_expression(f"D) {current_q['AnswerDText']}"), key="D"): handle_answer('D', current_q) st.rerun() # 복습 화면 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']) st.write("**✅ 정답:**", wrong_q['CorrectAnswer']) 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("### 🎯 유사 문제") #st.write(new_question['question']) display_math_question(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}) {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}) {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 not answered: with col1: for option in ['A', 'C']: if st.button( f"{option}) {format_math_expression(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}) {format_math_expression(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("
" * 5, unsafe_allow_html=True) # 5줄의 빈 줄 추가 st.markdown("""
""", 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("
" * 5, unsafe_allow_html=True) # 5줄의 빈 줄 추가 st.markdown("""
""", unsafe_allow_html=True) # 추가 여백 if __name__ == "__main__": main() # random_state 42에서 정답 # D C A A C # A B B B B