import os import streamlit as st import datetime # Not strictly used in this version, but often useful import google.generativeai as genai import time import google.api_core.exceptions # 오류 처리를 위해 추가 # --- Streamlit 설정 --- st.set_page_config( page_title="과학 탐구 도우미 🔬", page_icon="🔬", layout="wide", ) # --- Custom CSS (스타일링) --- st.markdown( """ """, unsafe_allow_html=True, ) # --- API 키 설정 --- api_key = os.environ.get("GEMINI_API_KEY") or st.secrets.get("GEMINI_API_KEY") if not api_key: st.error("Gemini API 키가 설정되지 않았습니다. 환경 변수 또는 Streamlit Secrets에 `GEMINI_API_KEY`를 설정해주세요.") st.stop() try: genai.configure(api_key=api_key) except Exception as e: st.error(f"API 키 설정 중 오류 발생: {e}") st.stop() # --- Gemini 모델 설정 --- generation_config = { "temperature": 0.6, "top_p": 0.9, "top_k": 50, "max_output_tokens": 8192, "response_mime_type": "text/plain", } MODEL_NAME = "gemini-1.5-flash-latest" try: model = genai.GenerativeModel( model_name=MODEL_NAME, generation_config=generation_config, ) except Exception as e: st.error(f"Gemini 모델 로딩 중 오류 발생: {e}") st.stop() # --- 세션 상태 초기화 --- default_values = { "section_index": 0, "max_reached_stage_index": 0, "grade_level": None, "student_interests": "", "topic_suggestions": None, "selected_topic_area": "", "research_question": "", "hypothesis": "", "exp_vars_independent": "", "exp_vars_dependent": "", "exp_vars_controlled": "", "exp_materials": "", "exp_procedure": "", "exp_data_collection": "", "exp_safety_precautions": "", "experiment_plan_draft": "", "ai_feedback": None, "chat_session": None, "chat_messages": [], "chat_history_api": [], "data_analysis_notes": "", "conclusion_draft": "", "presentation_tips": None, } for key, value in default_values.items(): if key not in st.session_state: st.session_state[key] = value # --- AI 안내 생성 함수 --- def get_ai_guidance(prompt_template, **kwargs): prompt = prompt_template.format(**kwargs) try: response = model.generate_content(prompt) guidance_text = response.text.strip().replace("**", "") return guidance_text except google.api_core.exceptions.ResourceExhausted as e: st.error(f"API 할당량 초과 오류. 잠시 후 다시 시도해주세요. 오류: {e}") return "죄송합니다. 지금은 AI 안내를 처리할 수 없습니다. (API 할당량 초과)" except Exception as e: st.error(f"AI 안내 생성 중 오류: {e}") return "죄송합니다. AI 안내 생성 중 예상치 못한 오류가 발생했습니다." # --- 프롬프트 템플릿 --- PROMPT_WELCOME_GRADE_INFO = """ 학생의 학년 수준에 맞는 과학 탐구 안내를 제공하려고 합니다. {grade_level} 학생들에게 과학 탐구가 왜 재미있고 중요한지, 그리고 이 AI 도우미가 어떻게 도움을 줄 수 있는지 간략하고 친근하게 설명해주세요. (2-3문장으로 요약) """ PROMPT_TOPIC_SUGGESTION = """ 당신은 {grade_level} 학생의 과학 탐구 주제 찾기를 돕는 창의적이고 친절한 AI 조수입니다. 학생의 관심사는 '{interests}' 입니다. 이 관심사와 학년 수준을 고려하여, 탐구해볼 만한 3-4가지 구체적인 탐구 주제 아이디어를 제안해주세요. 각 아이디어는 다음을 포함해야 합니다: 1. 흥미로운 주제명 (질문 형태 가능) 2. 간단한 탐구 내용 설명 (1-2문장) 3. 이 주제가 왜 {grade_level} 학생에게 적합한지 또는 재미있을지 이유 (1문장) 출력은 각 주제를 명확히 구분하고, 친근한 말투(~해요, ~ 어때요?)를 사용해주세요. """ PROMPT_QUESTION_HYPOTHESIS_FEEDBACK = """ 당신은 {grade_level} 학생의 과학 탐구 질문과 가설 설정을 돕는 숙련된 과학 멘토입니다. 학생이 선택한 탐구 분야는 '{topic_area}' 입니다. 학생이 작성한 탐구 질문은 다음과 같습니다: "{research_question}" 학생이 작성한 가설은 다음과 같습니다: "{hypothesis}" 다음 사항에 초점을 맞춰 구체적이고 건설적인 피드백을 제공해주세요: 1. **탐구 질문의 명확성 및 구체성:** 질문이 너무 광범위하거나 모호하지 않은가? 측정 가능하고 탐구 가능한 질문인가? 2. **가설의 타당성 및 검증 가능성:** 가설이 탐구 질문에 대한 합리적인 예측인가? 실험이나 관찰을 통해 검증할 수 있는 형태로 작성되었는가? ('만약 ~라면, ~일 것이다' 형식 권장) 3. **질문과 가설의 연관성:** 가설이 탐구 질문에 직접적으로 답하려는 시도인가? 정답을 직접 알려주기보다는, 학생이 스스로 생각하고 개선할 수 있도록 질문을 던지거나 제안하는 방식으로 피드백해주세요. 친절하고 격려하는 말투를 사용해주세요. (예: "좋은 시작이에요! 혹시 이 질문을 조금 더 구체적으로 만들어보면 어떨까요? 예를 들어...") """ PROMPT_EXPERIMENT_PLAN_FEEDBACK = """ 당신은 {grade_level} 학생의 과학 탐구 계획을 검토하고 피드백을 주는 경험 많은 과학 교사입니다. 학생의 탐구 질문은 "{research_question}"이고, 가설은 "{hypothesis}"입니다. 학생이 작성한 탐구 계획의 각 부분은 다음과 같습니다: 1. **독립 변인:** {exp_vars_independent} 2. **종속 변인:** {exp_vars_dependent} 3. **통제 변인:** {exp_vars_controlled} 4. **준비물:** {exp_materials} 5. **실험 절차:** {exp_procedure} 6. **데이터 수집 및 기록 방법:** {exp_data_collection} 7. **안전 수칙:** {exp_safety_precautions} 이 계획의 각 부분을 검토하고, 다음 사항에 초점을 맞춰 구체적이고 건설적인 피드백을 제공해주세요. 각 항목별로 나누어 설명해주면 좋습니다: - **변인 설정 및 통제:** 독립 변인, 종속 변인, 통제 변인이 명확하게 설정되었고, 통제 변인을 어떻게 일정하게 유지할 것인지 구체적인가? - **준비물:** 필요한 준비물이 모두 포함되었는가? 빠진 것은 없는가? - **실험 절차의 구체성 및 재현 가능성:** 다른 사람이 이 계획만 보고도 실험을 똑같이 따라 할 수 있을 만큼 절차가 상세하고 명확한가? 측정 방법, 측정 도구, 측정 횟수 등이 명시되었는가? - **데이터 수집 및 기록 방법:** 어떤 데이터를 수집할 것이며, 어떻게 기록할 것인지 명확한가? 표나 그림을 활용할 계획이 있는가? - **안전 수칙 고려:** 실험 과정에서 발생할 수 있는 안전 문제에 대해 고려하고 대비책을 마련했는가? (언급이 없다면 중요성을 강조) - **결과의 객관성 확보 방안:** 실험 결과를 객관적으로 얻기 위한 노력이 포함되어 있는가? (예: 반복 실험, 대조군 설정 등 - 절차에서 확인) 정답을 직접 알려주기보다는, 학생이 스스로 생각하고 계획을 개선할 수 있도록 질문을 던지거나 제안하는 방식으로 피드백해주세요. 친절하고 격려하는 말투를 사용해주세요. """ PROMPT_DATA_ANALYSIS_CONCLUSION_FEEDBACK = """ 당신은 {grade_level} 학생의 과학 탐구 데이터 분석 및 결론 도출을 돕는 데이터 분석 전문가이자 과학자입니다. 학생의 탐구 질문은 "{research_question}"이고, 가설은 "{hypothesis}"입니다. 학생이 작성한 데이터 분석 및 결과 요약은 다음과 같습니다: --- {data_analysis_notes} --- 학생이 작성한 결론 초안은 다음과 같습니다: --- {conclusion_draft} --- 다음 사항에 초점을 맞춰 구체적이고 건설적인 피드백을 제공해주세요: 1. **데이터 해석의 타당성:** 수집된 데이터를 바탕으로 결과를 올바르게 해석하고 있는가? 그래프나 표를 사용했다면 적절히 활용되었는가? 2. **결론과 가설의 연관성:** 도출된 결론이 수립했던 가설을 지지하는지, 반증하는지, 아니면 판단하기 어려운지 명확하게 언급하고 있는가? 3. **결론의 논리성 및 근거 제시:** 결론이 실험 결과(데이터)에 근거하여 논리적으로 도출되었는가? 4. **탐구의 한계점 및 제언 (선택 사항, 고학년의 경우):** 이번 탐구의 한계점은 무엇이었으며, 이를 보완하기 위해 어떤 추가 탐구를 해볼 수 있을지 제언하고 있는가? 학생이 자신의 결과를 더 깊이 이해하고 명확하게 표현할 수 있도록 도와주세요. 친절하고 격려하는 말투를 사용해주세요. """ PROMPT_PRESENTATION_TIPS = """ 당신은 {grade_level} 학생이 과학 탐구 결과를 효과적으로 발표할 수 있도록 돕는 커뮤니케이션 전문가입니다. 학생의 탐구 주제는 '{topic_area}' 이고, 주요 탐구 질문은 "{research_question}" 입니다. 학생이 탐구 발표(예: 과학 박람회 포스터, 구두 발표)를 준비할 때 유용한 팁을 5-7가지 정도 알려주세요. 팁은 다음 내용을 포함할 수 있습니다: - 발표 자료 구성 (제목, 탐구 동기, 가설, 과정, 결과, 결론, 제언 등) - 시각 자료 활용 (그림, 사진, 그래프 등) - 명확하고 간결한 설명 방법 - 예상 질문 대비 - 자신감 있는 태도 {grade_level} 학생이 이해하기 쉽고 실천하기 좋은 내용으로 구성해주세요. 친절하고 응원하는 말투를 사용해주세요. """ # --- 단계별 제목 및 아이콘 --- STAGE_TITLES_WITH_ICONS = [ ("👋", "환영 및 학년 선택"), ("💡", "탐구 주제 탐색"), ("❓", "탐구 질문 & 가설"), ("📝", "탐구 계획 설계"), ("🔬", "탐구 수행 & Q&A"), ("📊", "데이터 분석 & 결론"), ("📢", "탐구 발표 준비"), ("📚", "유용한 과학 자료실") ] # --- 네비게이션 및 페이지 렌더링 함수 --- def navigate_to_stage(stage_index): if stage_index > 0 and not st.session_state.grade_level: st.toast("먼저 '환영 및 학년 선택' 단계에서 학년을 선택해주세요.", icon="⚠️") return st.session_state.section_index = stage_index if stage_index > st.session_state.max_reached_stage_index: st.session_state.max_reached_stage_index = stage_index st.session_state.ai_feedback = None st.rerun() def render_prev_next_buttons(): # Determine button labels and states prev_button_label = "" next_button_label = "" prev_button_disabled = True next_button_disabled = True show_prev = st.session_state.section_index > 0 show_next = st.session_state.section_index < len(STAGE_TITLES_WITH_ICONS) - 1 if show_prev: prev_button_label = f"👈 이전: {STAGE_TITLES_WITH_ICONS[st.session_state.section_index-1][1]}" prev_button_disabled = False if show_next: next_button_label = f"다음: {STAGE_TITLES_WITH_ICONS[st.session_state.section_index+1][1]} 👉" # Basic check for proceeding current_stage = st.session_state.section_index if current_stage == 0: # 환영 next_button_disabled = not st.session_state.grade_level elif current_stage == 1: # 주제 탐색 next_button_disabled = not st.session_state.selected_topic_area elif current_stage == 2: # 질문/가설 next_button_disabled = not (st.session_state.research_question and st.session_state.hypothesis) elif current_stage == 3: # 계획 설계 next_button_disabled = not all([ st.session_state.exp_vars_independent, st.session_state.exp_vars_dependent, st.session_state.exp_vars_controlled, st.session_state.exp_materials, st.session_state.exp_procedure, st.session_state.exp_data_collection ]) elif current_stage == 4: # 수행/Q&A - always allow next for now next_button_disabled = False elif current_stage == 5: # 분석/결론 next_button_disabled = not (st.session_state.data_analysis_notes and st.session_state.conclusion_draft) elif current_stage == 6: # 발표 준비 - always allow next for now next_button_disabled = False else: # Should not happen if show_next is true next_button_disabled = False cols = st.columns([1, 1.5, 1]) # Adjusted for better balance with cols[0]: if show_prev: if st.button(prev_button_label, key=f"prev_stage_btn_{st.session_state.section_index}", use_container_width=True, type="secondary", disabled=prev_button_disabled, on_click=navigate_to_stage, args=(st.session_state.section_index - 1,)): pass with cols[2]: if show_next: if st.button(next_button_label, key=f"next_stage_btn_{st.session_state.section_index}", use_container_width=True, type="primary", disabled=next_button_disabled, on_click=navigate_to_stage, args=(st.session_state.section_index + 1,)): pass elif st.session_state.section_index == len(STAGE_TITLES_WITH_ICONS) - 1: # Last stage if st.button("💖 처음으로 돌아가기 (모든 내용 초기화)", use_container_width=True, key="reset_from_last_stage", type="primary", on_click=reset_app_state): pass def reset_app_state(): # Clear existing session state keys related to the app's logic keys_to_delete = list(default_values.keys()) # Get a list of keys from our defaults for key in keys_to_delete: if key in st.session_state: del st.session_state[key] # Re-initialize with defaults by re-running the script which will hit the init block st.toast("모든 탐구 내용이 초기화되었습니다. 새로운 탐구를 시작하세요!", icon="🔄") st.rerun() # --- 메인 화면 구성 함수 --- def main(): col_empty_left, col_main, col_empty_right = st.columns([0.05, 0.9, 0.05]) # Adjusted for wider main with st.sidebar: st.markdown("## 🔬 사이언스 탐구 도우미") st.caption("AI와 함께 즐거운 과학 탐구를 시작해요! 🚀") st.divider() st.markdown("#### 탐구 단계 네비게이터") for i, (icon, title) in enumerate(STAGE_TITLES_WITH_ICONS): is_active = st.session_state.section_index == i is_completed = i < st.session_state.max_reached_stage_index and i != st.session_state.section_index status_text = "" button_class_name = "sidebar-nav-button" # Base class for the """ st.markdown(button_html, unsafe_allow_html=True) st.divider() with st.expander("💡 앱 사용 가이드", expanded=False): st.markdown( """ **탐구 여정을 단계별로 안내해 드립니다:** - 사이드바의 네비게이터를 사용하여 각 단계로 이동할 수 있습니다. - (학년 선택은 필수입니다!) - 각 단계의 안내에 따라 탐구를 진행해보세요. """ ) for i, (icon, title) in enumerate(STAGE_TITLES_WITH_ICONS): st.markdown(f"**{i}. {icon} {title}**") st.markdown("---") st.markdown(f"

Version 1.2.1 (UI/UX Enhanced)

", unsafe_allow_html=True) with col_main: current_stage_index = st.session_state.section_index icon, title_text = STAGE_TITLES_WITH_ICONS[current_stage_index] # --- 0. 환영 및 학년 선택 --- if current_stage_index == 0: with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown("### 과학 탐구의 세계에 오신 것을 환영합니다! 🎉") st.markdown('

AI 탐구 도우미가 여러분의 호기심을 멋진 과학 탐구로 이끌어줄 거예요.
먼저 여러분의 학년을 선택해주세요. 맞춤형 안내를 제공해드립니다.

', unsafe_allow_html=True) st.divider() grade_options = ["선택하세요", "초등학교 3-4학년", "초등학교 5-6학년", "중학교 1학년", "중학교 2학년", "중학교 3학년", "고등학교 1학년", "고등학교 2학년", "고등학교 3학년"] current_grade_index = 0 if st.session_state.grade_level and st.session_state.grade_level in grade_options: current_grade_index = grade_options.index(st.session_state.grade_level) selected_grade = st.selectbox( "**👇 학년을 선택해주세요:**", grade_options, index=current_grade_index, key="grade_selector" ) if selected_grade != "선택하세요": if st.session_state.grade_level != selected_grade: st.session_state.grade_level = selected_grade st.toast(f"{selected_grade}으로 설정되었습니다! 다음 단계로 진행하세요.", icon="👍") st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 0) st.rerun() st.success(f"**{st.session_state.grade_level}**을 선택하셨습니다. 이제 탐구를 시작할 준비가 되었어요!") else: st.info("학년을 선택하면 탐구를 시작할 수 있습니다.") st.divider() render_prev_next_buttons() # --- 1. 탐구 주제 탐색 --- elif current_stage_index == 1: if not st.session_state.grade_level: st.warning("먼저 '환영 및 학년 선택' 단계에서 학년을 선택해주세요.") if st.button("🏠 학년 선택으로 돌아가기", key="back_to_grade_sel_1", use_container_width=True): navigate_to_stage(0) st.stop() with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown(f'

({st.session_state.grade_level} 대상) 어떤 분야에 관심이 있나요? AI가 재미있는 탐구 주제 아이디어를 찾아줄 거예요!

', unsafe_allow_html=True) render_prev_next_buttons() st.divider() st.session_state.student_interests = st.text_input( "**여러분의 관심사를 입력해주세요 (예: 식물, 우주, 로봇, 환경 오염 등):**", value=st.session_state.student_interests, key="interests_input", placeholder="자유롭게 입력하고 'AI에게 주제 요청' 버튼을 누르세요!" ) if st.button("✨ AI에게 주제 아이디어 요청하기", use_container_width=True, key="get_topic_ideas", disabled=not st.session_state.student_interests): if st.session_state.student_interests: with st.spinner("AI가 맞춤 주제를 찾고 있어요... 잠시만 기다려주세요! 🧠"): st.session_state.topic_suggestions = get_ai_guidance( PROMPT_TOPIC_SUGGESTION, grade_level=st.session_state.grade_level, interests=st.session_state.student_interests ) st.session_state.ai_feedback = None if st.session_state.topic_suggestions and "죄송합니다" not in st.session_state.topic_suggestions : st.toast("AI 주제 제안이 도착했어요!", icon="💡") else: st.toast("AI 주제 제안 중 문제가 발생했어요. 다시 시도해주세요.", icon="⚠️") if st.session_state.topic_suggestions: with st.expander("🤖 AI의 주제 제안 보기/숨기기", expanded=True): st.info(st.session_state.topic_suggestions) st.divider() st.markdown("##### 🎯 탐구할 주제 분야 선택 또는 입력") st.session_state.selected_topic_area = st.text_input( "**가장 마음에 드는 주제나 탐구하고 싶은 분야를 여기에 적어주세요:**", value=st.session_state.selected_topic_area, key="selected_topic_input", placeholder="AI 제안을 참고하거나, 직접 입력해주세요." ) if st.session_state.selected_topic_area: st.success(f"탐구 분야: **{st.session_state.selected_topic_area}**(으)로 진행합니다.") st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 1) elif st.session_state.student_interests and not st.session_state.topic_suggestions: st.info("위 입력창에 관심사를 적고 'AI에게 주제 아이디어 요청하기' 버튼을 눌러보세요.") elif not st.session_state.student_interests: st.info("먼저 관심사를 입력해주세요.") # --- 2. 탐구 질문 및 가설 설정 --- elif current_stage_index == 2: if not st.session_state.grade_level: st.warning("먼저 '환영 및 학년 선택' 단계에서 학년을 선택해주세요.") if st.button("🏠 학년 선택으로 돌아가기", key="back_to_grade_sel_2", use_container_width=True): navigate_to_stage(0) st.stop() if not st.session_state.selected_topic_area: st.warning("먼저 '탐구 주제 탐색' 단계에서 탐구하고 싶은 주제 분야를 정해주세요.") if st.button("💡 주제 탐색으로 돌아가기", key="back_to_topic_sel_2", use_container_width=True): navigate_to_stage(1) st.stop() with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown(f'

선택한 주제 분야 **"{st.session_state.selected_topic_area}"** 에 대해
구체적인 탐구 질문과 가설을 세워봅시다.

', unsafe_allow_html=True) render_prev_next_buttons() st.divider() st.session_state.research_question = st.text_area( "**1. 탐구 질문 작성하기:** 무엇을 알아보고 싶나요? (구체적으로 질문 형태로 작성)", value=st.session_state.research_question, height=100, key="research_question_input", placeholder="예: 햇빛의 양은 식물 성장에 어떤 영향을 미칠까?" ) st.session_state.hypothesis = st.text_area( "**2. 가설 세우기:** 탐구 질문에 대한 예상 답은 무엇인가요? ('만약 ~라면, ~일 것이다.' 형식 권장)", value=st.session_state.hypothesis, height=100, key="hypothesis_input", placeholder="예: 만약 햇빛을 많이 받는다면, 식물은 더 잘 자랄 것이다." ) if st.button("🧐 AI에게 질문 & 가설 피드백 받기", use_container_width=True, key="get_q_h_feedback", disabled=not (st.session_state.research_question and st.session_state.hypothesis)): with st.spinner("AI가 여러분의 질문과 가설을 검토 중입니다... 🔍"): st.session_state.ai_feedback = get_ai_guidance( PROMPT_QUESTION_HYPOTHESIS_FEEDBACK, grade_level=st.session_state.grade_level, topic_area=st.session_state.selected_topic_area, research_question=st.session_state.research_question, hypothesis=st.session_state.hypothesis ) if st.session_state.ai_feedback and "죄송합니다" not in st.session_state.ai_feedback: st.toast("AI 피드백이 도착했어요!", icon="👍") else: st.toast("AI 피드백 생성 중 문제가 발생했어요.", icon="⚠️") if st.session_state.ai_feedback: with st.expander("🤖 AI의 피드백 보기/숨기기", expanded=True): st.success(st.session_state.ai_feedback) if st.session_state.research_question and st.session_state.hypothesis: st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 2) elif not (st.session_state.research_question and st.session_state.hypothesis): st.info("탐구 질문과 가설을 모두 작성한 후 AI 피드백을 받거나 다음 단계로 진행할 수 있습니다.") # --- 3. 탐구 계획 설계 (Granular) --- elif current_stage_index == 3: if not st.session_state.grade_level: st.warning("먼저 '환영 및 학년 선택' 단계에서 학년을 선택해주세요.") if st.button("🏠 학년 선택으로 돌아가기", key="back_to_grade_sel_3", use_container_width=True): navigate_to_stage(0) st.stop() if not st.session_state.research_question or not st.session_state.hypothesis: st.warning("먼저 '탐구 질문 및 가설 설정' 단계에서 질문과 가설을 작성해주세요.") if st.button("❓ 질문/가설 설정으로 돌아가기", key="back_to_q_h_3", use_container_width=True): navigate_to_stage(2) st.stop() with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown(f'

탐구 질문: "{st.session_state.research_question}"
가설: "{st.session_state.hypothesis}"
어떻게 탐구를 진행할지 구체적인 계획을 단계별로 세워봅시다.

', unsafe_allow_html=True) render_prev_next_buttons() st.divider() with st.expander("**1. 변인 설정**", expanded=True): st.session_state.exp_vars_independent = st.text_input("독립 변인 (바꾸어 주는 조건):", value=st.session_state.exp_vars_independent, key="exp_vars_ind_input", placeholder="예: 햇빛의 양 (하루 2시간, 4시간, 6시간)") st.session_state.exp_vars_dependent = st.text_input("종속 변인 (측정하려는 결과):", value=st.session_state.exp_vars_dependent, key="exp_vars_dep_input", placeholder="예: 식물의 키, 잎의 수") st.session_state.exp_vars_controlled = st.text_area("통제 변인 (똑같이 유지할 조건):", value=st.session_state.exp_vars_controlled, key="exp_vars_ctrl_input", height=80, placeholder="예: 물의 양, 흙의 종류, 화분의 크기, 온도 (쉼표로 구분)") with st.expander("**2. 준비물**", expanded=True): st.session_state.exp_materials = st.text_area("필요한 준비물 목록:", value=st.session_state.exp_materials, key="exp_materials_input", height=100, placeholder="예: 강낭콩 씨앗 9개, 화분 3개, 흙, 물뿌리개, 자, 관찰일지 등") with st.expander("**3. 실험 절차**", expanded=True): st.session_state.exp_procedure = st.text_area("실험 방법 (순서대로 자세히):", value=st.session_state.exp_procedure, key="exp_procedure_input", height=200, placeholder="예:\n1. 각 화분에 씨앗 3개씩 심기\n2. 화분별 햇빛 양 다르게 조절\n3. 2주간 관찰 및 측정") with st.expander("**4. 데이터 수집 및 기록 방법**", expanded=True): st.session_state.exp_data_collection = st.text_area("데이터 수집 및 기록 방법:", value=st.session_state.exp_data_collection, key="exp_data_coll_input", height=80, placeholder="예: 3일마다 키(cm), 잎 수 측정 후 표 기록. 사진 촬영.") with st.expander("**5. 안전 수칙** (선택 사항이지만 중요!)", expanded=False): # Default closed st.session_state.exp_safety_precautions = st.text_area("실험 중 주의할 안전 수칙:", value=st.session_state.exp_safety_precautions, key="exp_safety_input", height=80, placeholder="예: 화분 떨어뜨리지 않기, 흙 만진 후 손 씻기") st.divider() all_plan_fields_filled = all([ st.session_state.exp_vars_independent, st.session_state.exp_vars_dependent, st.session_state.exp_vars_controlled, st.session_state.exp_materials, st.session_state.exp_procedure, st.session_state.exp_data_collection ]) if st.button("🛠️ AI에게 탐구 계획 피드백 받기", use_container_width=True, key="get_plan_feedback", disabled=not all_plan_fields_filled): with st.spinner("AI가 탐구 계획을 꼼꼼히 검토 중입니다... 🧐"): st.session_state.ai_feedback = get_ai_guidance( PROMPT_EXPERIMENT_PLAN_FEEDBACK, grade_level=st.session_state.grade_level, research_question=st.session_state.research_question, hypothesis=st.session_state.hypothesis, exp_vars_independent=st.session_state.exp_vars_independent, exp_vars_dependent=st.session_state.exp_vars_dependent, exp_vars_controlled=st.session_state.exp_vars_controlled, exp_materials=st.session_state.exp_materials, exp_procedure=st.session_state.exp_procedure, exp_data_collection=st.session_state.exp_data_collection, exp_safety_precautions=st.session_state.exp_safety_precautions ) if st.session_state.ai_feedback and "죄송합니다" not in st.session_state.ai_feedback: st.toast("AI 피드백이 도착했어요! 내용을 확인하고 계획을 보완해보세요.", icon="👍") else: st.toast("AI 피드백 생성 중 문제가 발생했어요.", icon="⚠️") if st.session_state.ai_feedback: with st.expander("🤖 AI의 피드백 보기/숨기기", expanded=True): st.info(st.session_state.ai_feedback) if all_plan_fields_filled: st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 3) else: st.info("탐구 계획의 필수 항목 (변인, 준비물, 절차, 데이터 수집)을 모두 작성한 후 AI 피드백을 받거나 다음 단계로 진행할 수 있습니다. 안전 수칙도 꼭 고려해주세요!") # --- 4. 탐구 수행 및 Q&A (챗봇) --- elif current_stage_index == 4: if not st.session_state.grade_level or not st.session_state.research_question: st.warning("필수 정보(학년, 탐구 질문 등)가 부족합니다. 이전 단계를 확인해주세요.") if st.button("🏠 처음으로 돌아가기", key="back_to_start_4", use_container_width=True): navigate_to_stage(0) st.stop() with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown('

탐구를 진행하면서 궁금한 점이나 어려운 점이 있다면 AI에게 언제든지 물어보세요!

', unsafe_allow_html=True) render_prev_next_buttons() st.divider() with st.expander("💬 나의 탐구 요약 (AI와 대화 시 참고)", expanded=False): st.markdown(f"**학년:** {st.session_state.grade_level}") st.markdown(f"**탐구 주제 분야:** {st.session_state.get('selected_topic_area', 'N/A')}") st.markdown(f"**탐구 질문:** {st.session_state.research_question}") st.markdown(f"**가설:** {st.session_state.hypothesis}") st.markdown(f"**독립 변인:** {st.session_state.get('exp_vars_independent', 'N/A')}") st.markdown(f"**종속 변인:** {st.session_state.get('exp_vars_dependent', 'N/A')}") # Chat UI message_container_div = st.container() with message_container_div: st.markdown('
', unsafe_allow_html=True) if not st.session_state.chat_messages: initial_ai_message_content = f""" 안녕하세요! '{st.session_state.get("selected_topic_area", "선택한 주제")}'에 대한 탐구를 도와드릴 준비가 되었어요. 현재 탐구 질문은 "{st.session_state.get("research_question", "정해지지 않음")}"이고, 가설은 "{st.session_state.get("hypothesis", "세우지 않음")}"이네요. 실험을 진행하거나 데이터를 분석하면서 궁금하거나 어려운 점이 있다면 언제든 저에게 물어보세요. 함께 고민해봐요! 😊 무엇을 도와드릴까요? """ st.session_state.chat_messages.append({"role": "assistant", "content": initial_ai_message_content}) st.session_state.chat_history_api.append({"role": "model", "parts": [{"text": initial_ai_message_content}]}) for msg in st.session_state.chat_messages: with st.chat_message(msg["role"]): st.markdown(msg["content"]) st.markdown('
', unsafe_allow_html=True) user_prompt = st.chat_input("AI 탐구 조수에게 질문해보세요... (Enter로 전송)", key="qa_chat_input") if user_prompt: st.session_state.chat_messages.append({"role": "user", "content": user_prompt}) st.session_state.chat_history_api.append({"role": "user", "parts": [{"text": user_prompt}]}) st.rerun() if st.session_state.chat_messages and st.session_state.chat_messages[-1]["role"] == "user": with st.chat_message("assistant"): with st.spinner("AI가 답변을 생각하고 있어요... 🤔"): try: if st.session_state.chat_session is None: valid_history = [] for h_item in st.session_state.chat_history_api[:-1]: if isinstance(h_item, dict) and "role" in h_item and "parts" in h_item: if isinstance(h_item["parts"], list) and all(isinstance(p, dict) and "text" in p for p in h_item["parts"]): valid_history.append(h_item) st.session_state.chat_session = model.start_chat(history=valid_history) chat_session = st.session_state.chat_session # Send only the last user message parts last_user_message_parts = st.session_state.chat_history_api[-1]["parts"] response = chat_session.send_message(last_user_message_parts) ai_response = response.text.strip() if not ai_response: ai_response = "음... 제가 지금은 답변을 드리기 어렵네요. 다시 질문해주시겠어요?" st.session_state.chat_messages.append({"role": "assistant", "content": ai_response}) st.session_state.chat_history_api.append({"role": "model", "parts": [{"text": ai_response}]}) st.markdown(ai_response) except google.api_core.exceptions.ResourceExhausted as e: st.error(f"API 할당량 초과. 잠시 후 다시 시도해주세요. 오류: {e}") error_msg = "죄송합니다. API 사용량 제한으로 지금은 답장을 드릴 수 없어요." st.session_state.chat_messages.append({"role": "assistant", "content": error_msg}) st.markdown(error_msg) except Exception as e: st.error(f"챗봇 응답 생성 중 오류: {e}") error_msg = f"죄송합니다. 지금은 답장을 드릴 수 없어요. (오류: {type(e).__name__})" st.session_state.chat_messages.append({"role": "assistant", "content": error_msg}) st.markdown(error_msg) st.rerun() st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 4) # --- 5. 데이터 분석 및 결론 도출 --- elif current_stage_index == 5: if not st.session_state.grade_level or not st.session_state.research_question: st.warning("필수 정보(학년, 탐구 질문 등)가 부족합니다. 이전 단계를 확인해주세요.") if st.button("🏠 처음으로 돌아가기", key="back_to_start_5", use_container_width=True): navigate_to_stage(0) st.stop() with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown(f'

탐구를 통해 얻은 데이터를 분석하고, 이를 바탕으로 결론을 내려봅시다.
탐구 질문: "{st.session_state.research_question}"
가설: "{st.session_state.hypothesis}"

', unsafe_allow_html=True) render_prev_next_buttons() st.divider() st.session_state.data_analysis_notes = st.text_area( "**1. 데이터 분석 및 결과 요약:** (수집 데이터 정리, 분석 내용, 주요 결과, 표/그래프 설명 등)", value=st.session_state.data_analysis_notes, height=150, key="data_analysis_input", placeholder="예: 각 화분별 식물 키 평균을 계산하여 막대 그래프로 나타냈습니다. 그 결과, 햇빛을 6시간 받은 식물의 평균 키가 가장 컸습니다." ) st.session_state.conclusion_draft = st.text_area( "**2. 결론 초안 작성:** (분석 결과 기반 결론, 가설과 비교 설명, 한계점 및 제언 등)", value=st.session_state.conclusion_draft, height=150, key="conclusion_input", placeholder="예: 이 탐구를 통해 햇빛의 양이 많을수록 식물이 더 잘 자란다는 것을 알 수 있었습니다. 이는 가설을 지지합니다. 추가 탐구로..." ) if st.button("📈 AI에게 분석 & 결론 피드백 받기", use_container_width=True, key="get_analysis_feedback", disabled=not (st.session_state.data_analysis_notes and st.session_state.conclusion_draft)): with st.spinner("AI가 여러분의 분석과 결론을 검토 중입니다... 📊"): st.session_state.ai_feedback = get_ai_guidance( PROMPT_DATA_ANALYSIS_CONCLUSION_FEEDBACK, grade_level=st.session_state.grade_level, research_question=st.session_state.research_question, hypothesis=st.session_state.hypothesis, data_analysis_notes=st.session_state.data_analysis_notes, conclusion_draft=st.session_state.conclusion_draft ) if st.session_state.ai_feedback and "죄송합니다" not in st.session_state.ai_feedback: st.toast("AI 피드백이 도착했어요!", icon="👍") else: st.toast("AI 피드백 생성 중 문제가 발생했어요.", icon="⚠️") if st.session_state.ai_feedback: with st.expander("🤖 AI의 피드백 보기/숨기기", expanded=True): st.info(st.session_state.ai_feedback) if st.session_state.data_analysis_notes and st.session_state.conclusion_draft: st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 5) else: st.info("데이터 분석 내용과 결론 초안을 모두 작성한 후 AI 피드백을 받거나 다음 단계로 진행할 수 있습니다.") # --- 6. 탐구 발표 준비 --- elif current_stage_index == 6: if not st.session_state.grade_level or not st.session_state.selected_topic_area: st.warning("필수 정보(학년, 탐구 주제 등)가 부족합니다. 이전 단계를 확인해주세요.") if st.button("🏠 처음으로 돌아가기", key="back_to_start_6", use_container_width=True): navigate_to_stage(0) st.stop() with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown(f'

멋진 탐구 결과를 효과적으로 발표할 수 있도록 AI가 도와줄게요!
탐구 주제: "{st.session_state.selected_topic_area}"
주요 탐구 질문: "{st.session_state.research_question}"

', unsafe_allow_html=True) render_prev_next_buttons() st.divider() if st.button("🎤 AI에게 발표 팁 요청하기", use_container_width=True, key="get_presentation_tips"): with st.spinner("AI가 발표 팁을 준비 중입니다... 💡"): st.session_state.presentation_tips = get_ai_guidance( PROMPT_PRESENTATION_TIPS, grade_level=st.session_state.grade_level, topic_area=st.session_state.selected_topic_area, research_question=st.session_state.research_question ) if st.session_state.presentation_tips and "죄송합니다" not in st.session_state.presentation_tips: st.toast("AI 발표 팁이 도착했어요!", icon="👍") else: st.toast("AI 발표 팁 생성 중 문제가 발생했어요.", icon="⚠️") if st.session_state.presentation_tips: with st.expander("🤖 AI의 발표 팁 보기/숨기기", expanded=True): st.info(st.session_state.presentation_tips) else: st.info("'AI에게 발표 팁 요청하기' 버튼을 눌러 유용한 조언을 받아보세요.") st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 6) # --- 7. 유용한 과학 자료실 --- elif current_stage_index == 7: with st.container(border=True): st.markdown(f'

{icon} {title_text}

', unsafe_allow_html=True) st.markdown('

과학 탐구에 도움이 될 만한 웹사이트들을 모아봤어요. 자유롭게 탐색해보세요!

', unsafe_allow_html=True) render_prev_next_buttons() st.divider() st.markdown("#### 🇰🇷 국내 주요 과학 교육 사이트") cols_kr = st.columns(2) with cols_kr[0]: with st.expander("사이언스올 (KOFAC)"): st.markdown("한국과학창의재단 운영, 과학 콘텐츠, 교육 자료, 행사 정보 제공.") st.link_button("🔗 사이언스올 바로가기", "https://www.scienceall.com/", use_container_width=True) with st.expander("국립중앙과학관"): st.markdown("온라인 전시, 교육 프로그램, 과학 소식 제공.") st.link_button("🔗 국립중앙과학관 바로가기", "https://www.science.go.kr/", use_container_width=True) with cols_kr[1]: with st.expander("EBS 사이언스"): st.markdown("재미있는 과학 영상 클립과 다큐멘터리 제공.") st.link_button("🔗 EBS 사이언스 바로가기", "https://www.ebs.co.kr/science", use_container_width=True) with st.expander("LG사이언스랜드"): st.markdown("어린이/청소년 과학 학습 사이트. 게임, 웹툰, 실험 정보 풍부.") st.link_button("🔗 LG사이언스랜드 바로가기", "https://www.lgsl.kr/", use_container_width=True) st.divider() st.markdown("#### 🌍 해외 유용 과학 사이트 (영문)") cols_world = st.columns(2) with cols_world[0]: with st.expander("Science Buddies"): st.markdown("과학 프로젝트 아이디어, 단계별 가이드, 과학 원리 설명 등 방대한 자료 제공.") st.link_button("🔗 Science Buddies 바로가기", "https://www.sciencebuddies.org/", use_container_width=True) with st.expander("National Geographic Kids"): st.markdown("동물, 자연, 과학 등 다양한 주제의 흥미로운 콘텐츠.") st.link_button("🔗 Nat Geo Kids 바로가기", "https://kids.nationalgeographic.com/", use_container_width=True) with cols_world[1]: with st.expander("NASA Space Place"): st.markdown("어린이/청소년 대상 우주 과학 사이트. 게임, 애니메이션, 만들기 활동.") st.link_button("🔗 NASA Space Place 바로가기", "https://spaceplace.nasa.gov/", use_container_width=True) st.divider() st.success("🎉 모든 탐구 단계를 완료하신 것을 축하합니다! 🎉\n이곳의 자료들을 활용해 더 넓은 과학의 세계를 탐험해보세요.") st.session_state.max_reached_stage_index = max(st.session_state.max_reached_stage_index, 7) if __name__ == "__main__": main()