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