sciencefair0 / app.py
ll7098ll's picture
Update app.py
c0a9c45 verified
import os
import streamlit as st
import datetime
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(
"""
<style>
/* 전체 폰트 변경 (Nanum Gothic, Google Fonts CDN 사용) */
@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700&display=swap');
body {
font-family: 'Nanum Gothic', sans-serif !important;
background-color: #f0f2f6; /* 약간 밝은 배경색 */
}
/* --- Chat UI 개선 (기존 스타일 유지 또는 약간 조정) --- */
.message-container {
max-height: 60vh;
overflow-y: auto;
padding: 15px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
border: 1px solid #d1d9e6; /* 테두리 색상 변경 */
border-radius: 10px;
background-color: #ffffff; /* 메시지 컨테이너 배경 흰색 */
}
.stChatMessage[data-testid="stChatMessage"] {
border-radius: 15px;
padding: 12px 18px;
margin-bottom: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.08);
width: fit-content;
max-width: 80%;
font-size: 1rem;
word-wrap: break-word;
border: none;
}
div[data-testid="stChatMessage"]:has(div[data-testid="stChatMessageContent"][aria-label="user message"]) {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: auto;
}
div[data-testid="stChatMessageContent"][aria-label="user message"] {
background-color: #cce5ff; /* 사용자 메시지 배경색 (연한 파랑) */
color: #004085;
}
div[data-testid="stChatMessage"]:has(div[data-testid="stChatMessageContent"][aria-label="assistant message"]) {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-right: auto;
}
div[data-testid="stChatMessageContent"][aria-label="assistant message"] {
background-color: #e9ecef; /* AI 메시지 배경색 (연한 회색) */
color: #383d41;
border: 1px solid #ced4da;
}
/* Toast message 스타일 */
div.streamlit-toast-container {
z-index: 10000;
}
div[data-testid="stToast"] {
border-radius: 15px;
padding: 20px;
box-shadow: 5px 5px 10px rgba(0,0,0,0.1);
}
/* 섹션 제목 스타일 */
.section-title {
font-size: 28px;
font-weight: bold;
color: #0056b3; /* 파란색 계열 */
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 3px dotted #007bff; /* 파란색 점선 */
}
/* 섹션 부제목 스타일 */
.section-subtitle {
font-size: 18px;
color: #5a6268;
margin-bottom: 15px;
font-weight: 500;
}
/* 버튼 스타일 */
div.stButton > button {
background-color: #007bff; /* 기본 파란색 버튼 */
color: white;
padding: 14px 28px;
font-size: 18px;
border-radius: 25px;
border: none;
box-shadow: 5px 5px 10px rgba(0,0,0,0.2);
transition: background-color 0.3s ease, transform 0.3s ease;
font-weight: bold;
}
div.stButton > button:hover {
background-color: #0056b3; /* 더 진한 파란색 */
transform: scale(1.07);
box-shadow: 5px 5px 15px rgba(0,0,0,0.3);
}
div.stButton > button:disabled {
background-color: #cccccc;
color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Expander 스타일 */
.streamlit-expanderHeader {
font-weight: bold;
color: #0056b3;
border-bottom: 2px dotted #007bff;
padding-bottom: 10px;
margin-bottom: 20px;
font-size: 17px;
}
/* Info, Success, Error, Warning Box 스타일 */
div.stInfo { /* 파란색 계열 Info */
border-radius: 15px; padding: 20px; margin-bottom: 20px;
box-shadow: 5px 5px 10px rgba(0,0,0,0.08);
border-left: 5px solid #17a2b8 !important;
background-color: #d1ecf1 !important;
border: 1px solid #bee5eb; color: #0c5460;
}
div.stSuccess { /* 초록색 계열 Success */
border-radius: 15px; padding: 20px; margin-bottom: 20px;
box-shadow: 5px 5px 10px rgba(0,0,0,0.08);
border-left: 5px solid #28a745 !important;
background-color: #d4edda !important;
border: 1px solid #c3e6cb; color: #155724;
}
div.stError { /* 빨간색 계열 Error */
border-radius: 15px; padding: 20px; margin-bottom: 20px;
box-shadow: 5px 5px 10px rgba(0,0,0,0.08);
border-left: 5px solid #dc3545 !important;
background-color: #f8d7da !important;
border: 1px solid #f5c6cb; color: #721c24;
}
div.stWarning { /* 노란색 계열 Warning */
border-radius: 15px; padding: 20px; margin-bottom: 20px;
box-shadow: 5px 5px 10px rgba(0,0,0,0.08);
border-left: 5px solid #ffc107 !important;
background-color: #fff3cd !important;
border: 1px solid #ffeeba; color: #856404;
}
</style>
""",
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 = "learnlm-2.0-flash-experimental"
try:
model = genai.GenerativeModel(
model_name=MODEL_NAME,
generation_config=generation_config,
)
except Exception as e:
st.error(f"Gemini 모델 로딩 중 오류 발생: {e}")
st.stop()
# --- 세션 상태 초기화 ---
if "section_index" not in st.session_state: st.session_state["section_index"] = 0
if "grade_level" not in st.session_state: st.session_state["grade_level"] = None
if "student_interests" not in st.session_state: st.session_state["student_interests"] = ""
if "topic_suggestions" not in st.session_state: st.session_state["topic_suggestions"] = None
if "selected_topic_area" not in st.session_state: st.session_state["selected_topic_area"] = ""
if "research_question" not in st.session_state: st.session_state["research_question"] = ""
if "hypothesis" not in st.session_state: st.session_state["hypothesis"] = ""
if "experiment_plan_draft" not in st.session_state: st.session_state["experiment_plan_draft"] = ""
if "ai_feedback" not in st.session_state: st.session_state["ai_feedback"] = None
if "chat_session" not in st.session_state: st.session_state["chat_session"] = None
if "chat_messages" not in st.session_state: st.session_state["chat_messages"] = []
if "chat_history_api" not in st.session_state: st.session_state["chat_history_api"] = []
if "data_analysis_notes" not in st.session_state: st.session_state["data_analysis_notes"] = ""
if "conclusion_draft" not in st.session_state: st.session_state["conclusion_draft"] = ""
if "presentation_tips" not in st.session_state: st.session_state["presentation_tips"] = None
# --- 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}"입니다.
학생이 작성한 탐구 계획 초안은 다음과 같습니다:
---
{experiment_plan_draft}
---
이 계획을 검토하고, 다음 사항에 초점을 맞춰 구체적이고 건설적인 피드백을 3-4가지 제공해주세요:
1. **변인 설정 및 통제:** 독립 변인, 종속 변인, 통제 변인이 명확하게 설정되었고, 통제 변인을 어떻게 일정하게 유지할 것인지 구체적인가?
2. **실험 절차의 구체성 및 재현 가능성:** 다른 사람이 이 계획만 보고도 실험을 똑같이 따라 할 수 있을 만큼 절차가 상세하고 명확한가? 측정 방법, 측정 도구, 측정 횟수 등이 명시되었는가?
3. **데이터 수집 및 기록 방법:** 어떤 데이터를 수집할 것이며, 어떻게 기록할 것인지 명확한가?
4. **안전 수칙 고려:** 실험 과정에서 발생할 수 있는 안전 문제에 대해 고려하고 대비책을 마련했는가? (필요시 언급)
5. **결과의 객관성 확보 방안:** 실험 결과를 객관적으로 얻기 위한 노력이 포함되어 있는가? (예: 반복 실험, 대조군 설정 등)
정답을 직접 알려주기보다는, 학생이 스스로 생각하고 계획을 개선할 수 있도록 질문을 던지거나 제안하는 방식으로 피드백해주세요.
친절하고 격려하는 말투를 사용해주세요.
"""
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} 학생이 이해하기 쉽고 실천하기 좋은 내용으로 구성해주세요.
친절하고 응원하는 말투를 사용해주세요.
"""
# --- 메인 화면 구성 함수 ---
def main():
col_empty_left, col_main, col_empty_right = st.columns([0.2, 3.6, 0.2])
with st.sidebar:
st.markdown("# 🔬 사이언스 탐구 도우미")
st.markdown("### AI와 함께 즐거운 과학 탐구를 시작해요!")
st.markdown("여러분의 호기심을 탐구로 연결하고, 과학적 사고력을 키울 수 있도록 AI가 도와줄 거예요. 🚀")
with st.expander("💡 앱 사용 가이드", expanded=False):
st.markdown(
"""
**탐구 여정을 단계별로 안내해 드립니다:**
**0단계: 환영 및 학년 선택 👋**
- 여러분의 학년을 선택해주세요. AI가 맞춤형 안내를 제공합니다.
**1단계: 탐구 주제 탐색 💡**
- 관심사를 입력하면 AI가 탐구 주제 아이디어를 제안해줘요.
- 마음에 드는 주제 분야를 선택하거나 AI와 더 이야기하며 주제를 정해요.
**2단계: 탐구 질문 및 가설 설정 ❓**
- 선택한 주제로 구체적인 탐구 질문과 가설을 만들어봐요.
- AI가 여러분의 질문과 가설을 더 명확하게 다듬도록 피드백을 줄 거예요.
**3단계: 탐구 계획 설계 📝**
- 어떻게 탐구를 진행할지 계획을 세워요 (변인 설정, 실험 방법, 준비물 등).
- AI가 꼼꼼한 계획을 세울 수 있도록 도와줄 거예요.
**4단계: 탐구 수행 및 Q&A 🔬**
- 탐구를 진행하면서 궁금한 점이나 어려운 점을 AI에게 물어보세요.
- AI 챗봇이 실시간으로 답변하고 함께 문제를 해결해 나갈 수 있도록 지원합니다.
**5단계: 데이터 분석 및 결론 도출 📊**
- 탐구를 통해 얻은 데이터를 분석하고 의미있는 결론을 내려봐요.
- AI가 데이터 해석과 결론 작성에 대한 조언을 해줄 거예요.
**6단계: 탐구 발표 준비 📢**
- 탐구 결과를 멋지게 발표할 수 있도록 AI가 발표 팁을 알려줘요.
**7단계: 유용한 과학 자료실 📚**
- 탐구에 도움이 될 만한 다양한 과학 관련 웹사이트 정보를 모아두었어요.
**✨ 각 단계를 차근차근 따라가면서 멋진 탐구를 완성해보세요! ✨**
"""
)
st.markdown("---")
st.markdown(f"<p style='font-size: 14px; color: #777;'>Version 1.0 (Science Helper)</p>", unsafe_allow_html=True)
with col_main:
nav_cols = st.columns([1, 6, 1])
with nav_cols[0]:
if st.session_state.section_index > 0:
if st.button("👈 이전 단계", key="prev_button_top", use_container_width=True):
st.session_state.section_index -= 1
st.session_state.ai_feedback = None
st.rerun()
# --- 0. 환영 및 학년 선택 ---
if st.session_state.section_index == 0:
with st.container(border=True):
st.markdown('<p class="section-title">👋 환영합니다! 과학 탐구 여정을 시작해볼까요?</p>', unsafe_allow_html=True)
st.markdown('<p class="section-subtitle">먼저 여러분의 학년을 선택해주세요. AI가 맞춤형으로 도와드릴게요.</p>', unsafe_allow_html=True)
grade_options = ["선택하세요", "초등학교 3-4학년", "초등학교 5-6학년", "중학교 1학년", "중학교 2학년", "중학교 3학년", "고등학교 1학년", "고등학교 2학년", "고등학교 3학년"]
selected_grade = st.selectbox(
"**학년을 선택해주세요:**",
grade_options,
index=grade_options.index(st.session_state.grade_level) if st.session_state.grade_level else 0,
key="grade_selector"
)
if selected_grade != "선택하세요":
if st.session_state.grade_level != selected_grade:
st.session_state.grade_level = selected_grade
# Optionally, display a welcome message from AI here
st.success(f"**{st.session_state.grade_level}**을 선택하셨습니다. 다음 단계로 진행해주세요!")
if st.button("다음: 탐구 주제 탐색하기 💡", use_container_width=True, key="start_topic_exploration", type="primary"):
st.session_state.section_index += 1
st.rerun()
else:
st.info("학년을 선택하면 탐구를 시작할 수 있습니다.")
# --- 1. 탐구 주제 탐색 ---
elif st.session_state.section_index == 1:
with st.container(border=True):
st.markdown('<p class="section-title">💡 탐구 주제 탐색하기</p>', unsafe_allow_html=True)
st.markdown(f'<p class="section-subtitle">({st.session_state.grade_level} 대상) 어떤 분야에 관심이 있나요? AI가 재미있는 탐구 주제를 찾아줄 거예요!</p>', unsafe_allow_html=True)
st.session_state.student_interests = st.text_input(
"**여러분의 관심사를 알려주세요 (예: 식물, 우주, 로봇, 환경 오염, 우리 동네 날씨 등):**",
value=st.session_state.student_interests,
key="interests_input",
placeholder="자유롭게 입력해보세요!"
)
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
else:
st.warning("관심사를 먼저 입력해주세요.")
if st.session_state.topic_suggestions:
st.markdown("---")
st.markdown("##### 🤖 AI의 주제 제안:")
st.info(st.session_state.topic_suggestions)
st.markdown("---")
st.session_state.selected_topic_area = st.text_input(
"**가장 마음에 드는 주제나 탐구하고 싶은 분야를 적어주세요 (AI 제안 참고 또는 자유롭게):**",
value=st.session_state.selected_topic_area,
key="selected_topic_input",
placeholder="예: '빛의 색깔에 따른 식물 성장 비교' 또는 '우리 학교 미세먼지 농도 측정'"
)
if st.session_state.selected_topic_area:
if st.button("다음: 탐구 질문 & 가설 설정하기 ❓", use_container_width=True, key="goto_question_hypothesis", type="primary"):
st.session_state.section_index += 1
st.session_state.topic_suggestions = None
st.rerun()
elif st.session_state.student_interests and not st.session_state.topic_suggestions:
st.info("AI에게 주제 아이디어를 요청해보세요!")
# --- 2. 탐구 질문 및 가설 설정 ---
elif st.session_state.section_index == 2:
with st.container(border=True):
st.markdown('<p class="section-title">❓ 탐구 질문 및 가설 설정</p>', unsafe_allow_html=True)
st.markdown(f'<p class="section-subtitle">선택한 주제 분야 **"{st.session_state.get("selected_topic_area", "아직 주제가 없어요!")}"** 에 대해 구체적인 탐구 질문과 가설을 세워봅시다.</p>', unsafe_allow_html=True)
if not st.session_state.get("selected_topic_area"):
st.warning("먼저 '탐구 주제 탐색' 단계에서 탐구하고 싶은 주제 분야를 정해주세요.")
if st.button("주제 탐색으로 돌아가기", key="back_to_topic_sel_2"):
st.session_state.section_index = 1
st.rerun()
else:
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:
st.markdown("---")
st.markdown("##### 🤖 AI의 피드백:")
st.success(st.session_state.ai_feedback)
st.markdown("---")
if st.session_state.research_question and st.session_state.hypothesis:
if st.button("다음: 탐구 계획 설계하기 📝", use_container_width=True, key="goto_experiment_plan", type="primary"):
st.session_state.section_index += 1
st.session_state.ai_feedback = None
st.rerun()
elif not (st.session_state.research_question and st.session_state.hypothesis):
st.info("탐구 질문과 가설을 모두 작성한 후 AI 피드백을 받거나 다음 단계로 진행할 수 있습니다.")
# --- 3. 탐구 계획 설계 ---
elif st.session_state.section_index == 3:
with st.container(border=True):
st.markdown('<p class="section-title">📝 탐구 계획 설계</p>', unsafe_allow_html=True)
st.markdown(f'<p class="section-subtitle">탐구 질문: "{st.session_state.research_question}" / 가설: "{st.session_state.hypothesis}"</p>', unsafe_allow_html=True)
st.markdown("어떻게 탐구를 진행할지 구체적인 계획을 세워봅시다. (변인 설정, 준비물, 실험 방법, 데이터 수집 방법, 안전 수칙 등)")
st.session_state.experiment_plan_draft = st.text_area(
"**탐구 계획 초안을 작성해주세요:** (자세할수록 좋아요!)",
value=st.session_state.experiment_plan_draft,
height=300,
key="experiment_plan_input",
placeholder="예시:\n1. 변인 설정:\n - 독립 변인: 햇빛의 양 (하루 2시간, 4시간, 6시간)\n - 종속 변인: 식물의 키, 잎의 수\n - 통제 변인: 물의 양, 흙의 종류, 화분의 크기, 온도\n2. 준비물: 강낭콩 씨앗 9개, 같은 크기의 화분 3개, 같은 종류의 흙, 물뿌리개, 자, 관찰일지 등\n3. 실험 방법:\n 가. 각 화분에 강낭콩 씨앗을 3개씩 심는다.\n 나. 한 화분은 하루 2시간, 다른 화분은 4시간, 나머지 화분은 6시간 동안 햇빛이 드는 곳에 둔다. (다른 조건 동일하게 유지)\n 다. 2주 동안 매일 같은 시간에 물을 주고, 3일 간격으로 식물의 키와 잎의 수를 측정하여 기록한다.\n4. 안전 수칙: 실험 중 화분을 떨어뜨리지 않도록 주의한다."
)
if st.button("AI에게 탐구 계획 피드백 받기 🛠️", use_container_width=True, key="get_plan_feedback", disabled=not st.session_state.experiment_plan_draft):
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,
experiment_plan_draft=st.session_state.experiment_plan_draft
)
if st.session_state.ai_feedback:
st.markdown("---")
st.markdown("##### 🤖 AI의 피드백:")
st.success(st.session_state.ai_feedback)
st.markdown("---")
if st.session_state.experiment_plan_draft:
if st.button("다음: 탐구 수행 및 Q&A 🔬", use_container_width=True, key="goto_execution_qa", type="primary"):
st.session_state.section_index += 1
st.session_state.ai_feedback = None
st.session_state.chat_messages = []
st.session_state.chat_history_api = []
st.session_state.chat_session = None
st.rerun()
else:
st.info("탐구 계획을 작성한 후 AI 피드백을 받거나 다음 단계로 진행할 수 있습니다.")
# --- 4. 탐구 수행 및 Q&A (챗봇) ---
elif st.session_state.section_index == 4:
with st.container(border=True):
st.markdown('<p class="section-title">🔬 탐구 수행 및 Q&A</p>', unsafe_allow_html=True)
st.markdown('<p class="section-subtitle">탐구를 진행하면서 궁금한 점이나 어려운 점이 있다면 AI에게 언제든지 물어보세요!</p>', unsafe_allow_html=True)
st.markdown(f"**현재 탐구 질문:** {st.session_state.research_question}\n\n**현재 가설:** {st.session_state.hypothesis}")
message_container_div = st.container()
with message_container_div:
st.markdown('<div class="message-container">', unsafe_allow_html=True) # Apply custom class
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": [initial_ai_message_content]})
# chat_session은 첫 사용자 입력 후 또는 여기서 바로 초기화 가능
# st.session_state.chat_session = model.start_chat(history=st.session_state.chat_history_api)
for msg in st.session_state.chat_messages:
with st.chat_message(msg["role"]):
st.write(msg["content"])
st.markdown('</div>', unsafe_allow_html=True)
if prompt := st.chat_input("AI 탐구 조수에게 질문해보세요...", key="qa_chat_input"):
st.session_state.chat_messages.append({"role": "user", "content": prompt})
st.session_state.chat_history_api.append({"role": "user", "parts": [prompt]})
with st.chat_message("assistant"):
with st.spinner("AI가 답변을 생각하고 있어요... 🤔"):
try:
if st.session_state.chat_session is None:
# 시스템 프롬프트 추가 (모델이 지원하는 경우)
# system_instruction = f"You are a helpful AI assistant for a {st.session_state.grade_level} student conducting a science experiment. Their research question is '{st.session_state.research_question}' and hypothesis is '{st.session_state.hypothesis}'. Guide them through their experiment and data analysis. Be encouraging and ask clarifying questions."
# For Gemini, system instructions are often part of the model config or initial history.
# We'll rely on the context within the chat history.
st.session_state.chat_session = model.start_chat(
history=st.session_state.chat_history_api[:-1] # Pass history up to the last user message
)
chat_session = st.session_state.chat_session
response = chat_session.send_message(prompt) # Send only the new prompt
ai_response = response.text.strip()
st.session_state.chat_messages.append({"role": "assistant", "content": ai_response})
st.session_state.chat_history_api.append({"role": "model", "parts": [ai_response]})
st.write(ai_response) # Display 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.write(error_msg)
except Exception as e:
st.error(f"챗봇 응답 생성 중 오류: {e}")
error_msg = "죄송합니다. 지금은 답장을 드릴 수 없어요."
st.session_state.chat_messages.append({"role": "assistant", "content": error_msg})
st.write(error_msg)
st.rerun() # Re-run to update the chat display immediately
st.markdown("---")
if st.button("다음: 데이터 분석 및 결론 도출하기 📊", use_container_width=True, key="goto_data_analysis", type="primary"):
st.session_state.section_index += 1
st.rerun()
# --- 5. 데이터 분석 및 결론 도출 ---
elif st.session_state.section_index == 5:
with st.container(border=True):
st.markdown('<p class="section-title">📊 데이터 분석 및 결론 도출</p>', unsafe_allow_html=True)
st.markdown(f'<p class="section-subtitle">탐구를 통해 얻은 데이터를 분석하고, 이를 바탕으로 결론을 내려봅시다.</p>', unsafe_allow_html=True)
st.markdown(f"**탐구 질문:** {st.session_state.research_question}\n\n**가설:** {st.session_state.hypothesis}")
st.session_state.data_analysis_notes = st.text_area(
"**1. 데이터 분석 및 결과 요약:** 수집한 데이터를 어떻게 정리하고 분석했나요? 주요 결과는 무엇인가요? (표, 그래프 설명 등)",
value=st.session_state.data_analysis_notes,
height=200,
key="data_analysis_input",
placeholder="예: 각 화분별 식물 키 평균을 계산하여 막대 그래프로 나타냈습니다. 그 결과, 햇빛을 6시간 받은 식물의 평균 키가 가장 컸습니다."
)
st.session_state.conclusion_draft = st.text_area(
"**2. 결론 초안 작성:** 분석 결과를 바탕으로 어떤 결론을 내릴 수 있나요? 가설과 비교하여 설명해주세요.",
value=st.session_state.conclusion_draft,
height=200,
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:
st.markdown("---")
st.markdown("##### 🤖 AI의 피드백:")
st.success(st.session_state.ai_feedback)
st.markdown("---")
if st.session_state.data_analysis_notes and st.session_state.conclusion_draft:
if st.button("다음: 탐구 발표 준비하기 📢", use_container_width=True, key="goto_presentation_prep", type="primary"):
st.session_state.section_index += 1
st.session_state.ai_feedback = None
st.rerun()
else:
st.info("데이터 분석 내용과 결론 초안을 모두 작성한 후 AI 피드백을 받거나 다음 단계로 진행할 수 있습니다.")
# --- 6. 탐구 발표 준비 ---
elif st.session_state.section_index == 6:
with st.container(border=True):
st.markdown('<p class="section-title">📢 탐구 발표 준비</p>', unsafe_allow_html=True)
st.markdown(f'<p class="section-subtitle">멋진 탐구 결과를 효과적으로 발표할 수 있도록 AI가 도와줄게요!</p>', unsafe_allow_html=True)
st.markdown(f"**탐구 주제:** {st.session_state.selected_topic_area}\n\n**주요 탐구 질문:** {st.session_state.research_question}")
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:
st.markdown("---")
st.markdown("##### 🤖 AI의 발표 팁:")
st.info(st.session_state.presentation_tips)
st.markdown("---")
if st.button("다음: 유용한 과학 자료실 보기 📚", use_container_width=True, key="goto_resources", type="primary"):
st.session_state.section_index += 1
st.session_state.presentation_tips = None # 팁 초기화
st.rerun()
# --- 7. 유용한 과학 자료실 ---
elif st.session_state.section_index == 7:
with st.container(border=True):
st.markdown('<p class="section-title">📚 유용한 과학 자료실</p>', unsafe_allow_html=True)
st.markdown('<p class="section-subtitle">과학 탐구에 도움이 될 만한 웹사이트들을 모아봤어요. 자유롭게 탐색해보세요!</p>', unsafe_allow_html=True)
st.markdown("#### 🇰🇷 국내 주요 과학 교육 사이트")
with st.expander("사이언스올 (KOFAC)"):
st.markdown("- **설명:** 한국과학창의재단에서 운영하는 대표적인 과학 교육 포털입니다. 다양한 과학 콘텐츠, 교육 자료, 과학 행사 정보를 제공합니다.")
st.link_button("사이언스올 바로가기", "https://www.scienceall.com/")
with st.expander("EBS 사이언스 (EBS Science)"):
st.markdown("- **설명:** EBS에서 제공하는 과학 교육 채널 및 웹사이트입니다. 재미있는 과학 영상 클립과 다큐멘터리를 볼 수 있습니다.")
st.link_button("EBS 사이언스 바로가기", "https://www.ebs.co.kr/science") # URL 확인 필요
with st.expander("국립중앙과학관"):
st.markdown("- **설명:** 우리나라 대표 과학관으로, 온라인 전시, 교육 프로그램, 과학 소식 등을 제공합니다.")
st.link_button("국립중앙과학관 바로가기", "https://www.science.go.kr/")
with st.expander("LG사이언스랜드"):
st.markdown("- **설명:** LG에서 운영하는 어린이/청소년 과학 학습 사이트입니다. 재미있는 과학 게임, 웹툰, 실험 정보가 많습니다.")
st.link_button("LG사이언스랜드 바로가기", "https://www.lgsl.kr/")
st.markdown("#### 🌍 해외 유용 과학 사이트 (영문)")
with st.expander("Science Buddies"):
st.markdown("- **설명:** 학생들의 과학 프로젝트 아이디어, 단계별 가이드, 과학 원리 설명 등 방대한 자료를 제공하는 유명 사이트입니다.")
st.link_button("Science Buddies 바로가기", "https://www.sciencebuddies.org/")
with st.expander("NASA Space Place"):
st.markdown("- **설명:** NASA에서 운영하는 어린이/청소년 대상 우주 과학 사이트입니다. 재미있는 게임, 애니메이션, 만들기 활동이 많습니다.")
st.link_button("NASA Space Place 바로가기", "https://spaceplace.nasa.gov/")
with st.expander("National Geographic Kids"):
st.markdown("- **설명:** 내셔널 지오그래픽의 어린이 버전으로, 동물, 자연, 과학, 역사 등 다양한 주제의 흥미로운 콘텐츠를 제공합니다.")
st.link_button("Nat Geo Kids 바로가기", "https://kids.nationalgeographic.com/")
st.markdown("---")
st.success("탐구 여정을 성공적으로 마친 것을 축하합니다! 🎉\n이곳의 자료들을 활용해 더 넓은 과학의 세계를 탐험해보세요.")
if st.button("💖 처음으로 돌아가기", use_container_width=True, key="reset_button_final", type="primary"):
# 모든 관련 세션 상태 초기화
keys_to_reset = [
"grade_level", "student_interests", "topic_suggestions",
"selected_topic_area", "research_question", "hypothesis",
"experiment_plan_draft", "ai_feedback", "chat_session",
"chat_messages", "chat_history_api", "data_analysis_notes",
"conclusion_draft", "presentation_tips"
]
for key in keys_to_reset:
if key in st.session_state:
st.session_state[key] = None if key in ["topic_suggestions", "ai_feedback", "chat_session", "presentation_tips"] else "" if isinstance(st.session_state[key], str) else [] if isinstance(st.session_state[key], list) else None
st.session_state.chat_messages = []
st.session_state.chat_history_api = []
st.session_state.student_interests = ""
st.session_state.selected_topic_area = ""
st.session_state.research_question = ""
st.session_state.hypothesis = ""
st.session_state.experiment_plan_draft = ""
st.session_state.data_analysis_notes = ""
st.session_state.conclusion_draft = ""
st.session_state.section_index = 0
st.rerun()
if __name__ == "__main__":
main()