| | import os |
| | import openai |
| | import streamlit as st |
| | |
| |
|
| | |
| | openai_api_key = st.secrets["OPENAI_API_KEY"] |
| |
|
| |
|
| | |
| | if not openai_api_key: |
| | st.error("OpenAI API 키가 설정되지 않았습니다. 환경 변수나 Streamlit secrets에 키를 추가해주세요.") |
| | st.stop() |
| |
|
| | openai.api_key = openai_api_key |
| |
|
| | |
| |
|
| | def generate_smart_system_prompt(grade_level): |
| | """학년 수준에 맞는 SMART 목표 설정 시스템 프롬프트를 생성합니다.""" |
| |
|
| | |
| | try: |
| | parts = grade_level.split(" ") |
| | if len(parts) == 2: |
| | school_level = parts[0] |
| | grade_num = parts[1].rstrip("학년") |
| | if school_level not in ["초등학교", "중학교", "고등학교"] or not grade_num.isdigit(): |
| | grade_num, school_level = "알 수 없음", "알 수 없음" |
| | else: |
| | grade_num, school_level = "알 수 없음", "알 수 없음" |
| | except Exception: |
| | grade_num, school_level = "알 수 없음", "알 수 없음" |
| |
|
| | |
| | prompt = f""" |
| | 너는 {grade_level} 학생이 SMART 목표를 세우고 실천 계획을 만들도록 돕는 친절하고 격려하는 코치 선생님이야. |
| | 학생의 이름은 부르지 않고, '친구' 또는 '학생'이라고 불러줘. 반말로 친근하게 대화해줘. |
| | 학생이 이루고 싶은 목표나 상황을 이야기하면, 그 목표가 SMART 기준에 맞도록 자연스럽게 질문을 던져줘. |
| | SMART는 목표를 더 명확하고 달성 가능하게 만드는 방법이야: |
| | - S (Specific - 구체적인): 목표가 명확하고 자세한가? 무엇을 이루고 싶은지 정확히 아는 거야. |
| | - M (Measurable - 측정 가능한): 목표를 달성했는지 어떻게 알 수 있을까? 숫자로 표현할 수 있으면 좋아. |
| | - A (Achievable - 달성 가능한): 이 목표를 실제로 이룰 수 있을까? 너무 어렵거나 쉽지 않게 설정하는 거야. |
| | - R (Relevant - 관련성 있는): 이 목표가 왜 중요할까? 나에게 의미가 있는 목표여야 해. |
| | - T (Time-bound - 시간 제한이 있는): 언제까지 이 목표를 이루고 싶니? 마감일을 정하는 거야. |
| | |
| | 절대 네가 목표나 계획을 직접 제시하거나 정답을 알려주지 마. |
| | 대신, 학생 스스로 생각하고 답을 찾도록 소크라테스식 질문을 사용해줘. 예를 들면: |
| | - "우와, 좋은 생각인데! 그 목표를 조금 더 자세하게 설명해 줄 수 있을까?" (Specific 유도) |
| | - "목표를 이루면 어떤 모습일지 상상해볼래? 그걸 어떻게 확인할 수 있을까?" (Measurable 유도) |
| | - "그 목표를 이루려면 어떤 노력이 필요할까? 혹시 도움이 필요한 부분이 있을까?" (Achievable 유도) |
| | - "이 목표가 친구에게 왜 그렇게 중요해?" (Relevant 유도) |
| | - "언제까지 그 목표를 딱! 이루고 싶어?" (Time-bound 유도) |
| | - "좋아, 그럼 이제 그 목표를 이루기 위해 어떤 작은 단계들을 하나씩 해볼 수 있을까?" (실천 계획 유도) |
| | |
| | 학생이 목표를 정하는 과정에서 어려움을 느끼거나 주제에서 벗어나면 부드럽게 다시 목표 설정으로 이끌어줘. |
| | 학생의 대답을 칭찬하고 격려하며 자신감을 심어줘. |
| | 한 번에 너무 많은 질문을 하지 말고, 학생의 대답을 듣고 다음 질문으로 넘어가줘. |
| | |
| | 학생이 SMART 기준에 맞춰 목표를 구체화하고, 그 목표를 달성하기 위한 실천 계획 (최소 3가지 구체적인 행동)까지 스스로 만들었다고 판단되면, |
| | 마지막에 학생이 직접 세운 내용을 명확하게 요약해서 보여줘. |
| | 요약 예시: "정말 멋지다! 친구가 직접 세운 SMART 목표와 실천 계획을 함께 정리해볼까? \\n\\n**🎯 SMART 목표:** [학생이 정의한 구체적이고, 측정 가능하며, 달성 가능하고, 관련성 있고, 시간 제한이 있는 목표 요약]\\n\\n**👣 실천 계획:**\\n1. [학생이 정의한 첫 번째 실천 단계]\\n2. [학생이 정의한 두 번째 실천 단계]\\n3. [학생이 정의한 세 번째 실천 단계]\\n\\n이렇게 계획을 세우니 목표가 훨씬 가까워진 느낌이지? 꾸준히 실천하면 꼭 이룰 수 있을 거야! 선생님이 응원할게! 😊" |
| | 요약하기 전에는 반드시 "이제 목표랑 실천 계획이 다 세워진 것 같은데, 선생님이 한번 정리해봐도 괜찮을까?" 와 같이 학생의 동의를 구하는 질문을 먼저 해줘. |
| | """ |
| | return prompt |
| |
|
| | def openai_chat(grade_level): |
| | """OpenAI API를 호출하여 채팅 응답을 생성합니다.""" |
| | try: |
| | |
| | system_prompt = generate_smart_system_prompt(grade_level) |
| |
|
| | |
| | if not st.session_state.messages or st.session_state.messages[0]["role"] != "system": |
| | |
| | st.session_state.messages.insert(0, {"role": "system", "content": system_prompt}) |
| | else: |
| | |
| | st.session_state.messages[0]["content"] = system_prompt |
| |
|
| | |
| | response = openai.ChatCompletion.create( |
| | model="gpt-4o", |
| | messages=st.session_state.messages, |
| | temperature=0.7, |
| | max_tokens=2000, |
| | top_p=0.9, |
| | frequency_penalty=0.1, |
| | presence_penalty=0.1 |
| | ) |
| | return response.choices[0].message["content"] |
| | except openai.error.OpenAIError as e: |
| | st.error(f"OpenAI API 오류 발생: {str(e)}") |
| | return None |
| | except Exception as e: |
| | st.error(f"알 수 없는 오류 발생: {str(e)}") |
| | return None |
| |
|
| | |
| | st.set_page_config( |
| | page_title="SMART 목표 설정 도우미", |
| | page_icon="🎯", |
| | initial_sidebar_state="expanded" |
| | ) |
| |
|
| | |
| | st.markdown( |
| | """ |
| | <style> |
| | /* 전체 배경색 설정 */ |
| | .stApp { |
| | background-color: #f0f8ff; /* 부드러운 하늘색 배경 */ |
| | } |
| | /* 타이틀 스타일 */ |
| | .main-title { |
| | font-size: 2.5rem; |
| | color: #4682b4; /* 차분한 파란색 */ |
| | font-weight: 700; |
| | text-align: center; |
| | margin-bottom: 20px; |
| | } |
| | /* 채팅 메시지 컨테이너 */ |
| | .chat-message { |
| | border-radius: 15px; |
| | padding: 12px 15px; /* 패딩 약간 조정 */ |
| | margin: 10px 0; |
| | display: flex; |
| | align-items: flex-start; /* 아이콘과 텍스트 상단 정렬 */ |
| | flex-wrap: nowrap; |
| | word-break: break-word; |
| | max-width: 85%; /* 메시지 최대 너비 */ |
| | box-shadow: 0 2px 4px rgba(0,0,0,0.05); /* 약간의 그림자 효과 */ |
| | } |
| | /* 사용자 메시지 스타일 */ |
| | .chat-message-user { |
| | background-color: #e0f7fa; /* 밝은 청록색 */ |
| | color: #00796b; /* 어두운 청록색 */ |
| | margin-left: auto; /* 오른쪽 정렬 */ |
| | flex-direction: row-reverse; /* 내용과 아이콘 순서 변경 */ |
| | } |
| | /* AI 메시지 스타일 */ |
| | .chat-message-assistant { |
| | background-color: #fff0f5; /* 라벤더 블러시 */ |
| | color: #c71585; /* 미디엄 바이올렛 레드 */ |
| | margin-right: auto; /* 왼쪽 정렬 */ |
| | flex-direction: row; /* 기본 순서 (아이콘 먼저) */ |
| | } |
| | /* 아바타(아이콘) 스타일 */ |
| | .chat-avatar { |
| | width: 40px; |
| | height: 40px; |
| | border-radius: 50%; |
| | margin-right: 10px; /* AI 메시지 아이콘 오른쪽 여백 */ |
| | flex-shrink: 0; /* 아이콘 크기 고정 */ |
| | } |
| | /* 사용자 아바타 스타일 */ |
| | .chat-avatar-user { |
| | margin-left: 10px; /* 사용자 메시지 아이콘 왼쪽 여백 */ |
| | margin-right: 0; |
| | } |
| | /* 채팅 내용 스타일 */ |
| | .chat-content { |
| | flex-grow: 1; /* 텍스트 영역이 남은 공간 차지 */ |
| | /* 텍스트 선택 가능하게 (기본값) */ |
| | user-select: text; |
| | -webkit-user-select: text; |
| | -moz-user-select: text; |
| | -ms-user-select: text; |
| | } |
| | /* 사용자 입력 창 스타일 */ |
| | .stTextInput input { |
| | border-radius: 15px; |
| | border: 2px solid #add8e6; /* 밝은 파란색 */ |
| | padding: 10px 15px; |
| | } |
| | /* 버튼 스타일 */ |
| | .stButton button { |
| | background-color: #4682b4; /* 차분한 파란색 */ |
| | color: #fff; |
| | border-radius: 15px; |
| | padding: 10px 20px; |
| | border: none; |
| | transition: background-color 0.2s ease; /* 부드러운 색상 변경 효과 */ |
| | } |
| | .stButton button:hover { |
| | background-color: #5a9bd3; /* 호버 시 약간 밝게 */ |
| | } |
| | .stButton button:active { |
| | background-color: #3e74a0; /* 클릭 시 약간 어둡게 */ |
| | } |
| | </style> |
| | """, |
| | unsafe_allow_html=True |
| | ) |
| |
|
| | |
| | st.markdown("<div class='main-title'>🎯 SMART 목표 설정 도우미 ✍️</div>", unsafe_allow_html=True) |
| |
|
| | |
| | with st.sidebar: |
| | st.header("⚙️ 설정") |
| | |
| | grade_level_options = [ |
| | "초등학교 1학년", "초등학교 2학년", "초등학교 3학년", "초등학교 4학년", "초등학교 5학년", "초등학교 6학년", |
| | "중학교 1학년", "중학교 2학년", "중학교 3학년", |
| | "고등학교 1학년", "고등학교 2학년", "고등학교 3학년" |
| | ] |
| | |
| | try: |
| | default_index = grade_level_options.index("초등학교 6학년") |
| | except ValueError: |
| | default_index = 5 |
| |
|
| | |
| | selected_grade = st.selectbox( |
| | "👤 학생의 학년을 선택하세요:", |
| | grade_level_options, |
| | index=default_index, |
| | key="grade_select" |
| | ) |
| |
|
| | |
| | if st.button("🔄 대화 초기화"): |
| | |
| | st.session_state.messages = [] |
| | st.success("대화 내용이 초기화되었습니다. 새로운 목표를 설정해보세요!") |
| | |
| | st.rerun() |
| |
|
| | st.info("💡 AI 코치가 질문을 통해 스스로 SMART 목표와 실천 계획을 세우도록 도와줄 거예요!") |
| | st.markdown("---") |
| | st.caption("Powered by OpenAI GPT-4o") |
| |
|
| | |
| |
|
| | |
| | if "messages" not in st.session_state: |
| | st.session_state.messages = [] |
| |
|
| | |
| | user_icon_url = "https://cdn-icons-png.flaticon.com/512/1995/1995531.png" |
| | assistant_icon_url = "https://cdn-icons-png.flaticon.com/512/4323/4323008.png" |
| |
|
| | |
| | if not st.session_state.messages: |
| | |
| | system_prompt = generate_smart_system_prompt(selected_grade) |
| | st.session_state.messages.append({"role": "system", "content": system_prompt}) |
| | |
| | welcome_message = "안녕! 👋 나는 네 목표 설정을 도와줄 AI 코치 선생님이야. 이루고 싶은 목표나 하고 싶은 일이 있으면 나에게 이야기해 줄래? 같이 멋진 계획을 세워보자! 😊" |
| | st.session_state.messages.append({"role": "assistant", "content": welcome_message}) |
| | |
| |
|
| | |
| | |
| | for index, message in enumerate(st.session_state.messages): |
| | if message["role"] == "system": |
| | continue |
| |
|
| | role = message["role"] |
| | content = message["content"] |
| | |
| |
|
| | |
| | if role == "user": |
| | |
| | st.markdown( |
| | f""" |
| | <div class='chat-message chat-message-user' key='user_msg_{index}'> |
| | <div class="chat-content">{content}</div> |
| | <img src='{user_icon_url}' class='chat-avatar chat-avatar-user'> |
| | </div> |
| | """, |
| | unsafe_allow_html=True |
| | ) |
| | elif role == "assistant": |
| | |
| | st.markdown( |
| | f""" |
| | <div class='chat-message chat-message-assistant' key='assistant_msg_{index}'> |
| | <img src='{assistant_icon_url}' class='chat-avatar'> |
| | <div class="chat-content">{content}</div> |
| | </div> |
| | """, |
| | unsafe_allow_html=True |
| | ) |
| |
|
| | |
| | if prompt := st.chat_input("🎯 이루고 싶은 목표나 하고 싶은 일을 적어보세요! (예: 수학 시험 잘 보기)"): |
| | |
| | st.session_state.messages.append({"role": "user", "content": prompt}) |
| |
|
| | |
| | with st.spinner("AI 코치가 생각 중이에요... 🤔"): |
| | response = openai_chat(selected_grade) |
| |
|
| | |
| | if response: |
| | st.session_state.messages.append({"role": "assistant", "content": response}) |
| | |
| |
|
| | |
| | st.rerun() |