Update app.py
Browse files
app.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
import os
|
| 2 |
import openai
|
| 3 |
import streamlit as st
|
|
|
|
| 4 |
|
| 5 |
-
# OpenAI API 설정
|
| 6 |
# Streamlit Cloud secrets에서 API 키 가져오기
|
|
|
|
| 7 |
openai_api_key = st.secrets["OPENAI_API_KEY"]
|
| 8 |
# 로컬 환경 변수에서 API 키 가져오기 (선택 사항)
|
| 9 |
# openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
@@ -11,33 +13,29 @@ openai_api_key = st.secrets["OPENAI_API_KEY"]
|
|
| 11 |
# API 키가 있는지 확인
|
| 12 |
if not openai_api_key:
|
| 13 |
st.error("OpenAI API 키가 설정되지 않았습니다. 환경 변수나 Streamlit secrets에 키를 추가해주세요.")
|
| 14 |
-
st.stop()
|
| 15 |
|
| 16 |
openai.api_key = openai_api_key
|
| 17 |
|
|
|
|
|
|
|
| 18 |
def generate_smart_system_prompt(grade_level):
|
| 19 |
"""학년 수준에 맞는 SMART 목표 설정 시스템 프롬프트를 생성합니다."""
|
| 20 |
-
|
| 21 |
-
# 학년 정보 추출 (예: "초등학교 6학년" -> "6")
|
| 22 |
try:
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
school_level =
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
elif grade_level.startswith("고등학교"):
|
| 30 |
-
grade_num = grade_level.split(" ")[1].rstrip("학년")
|
| 31 |
-
school_level = "고등학교"
|
| 32 |
else:
|
| 33 |
-
grade_num = "알 수 없음"
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
grade_num = "알 수 없음"
|
| 37 |
-
school_level = "알 수 없음"
|
| 38 |
|
| 39 |
-
# 초등학교 6학년
|
| 40 |
-
# (요청에 따라 초등학교 6학년에 더 집중하도록 수정)
|
| 41 |
if school_level == "초등학교" and grade_num == "6":
|
| 42 |
prompt = f"""
|
| 43 |
너는 초등학교 6학년 학생이 SMART 목표를 세우고 실천 계획을 만들도록 돕는 친절하고 격려하는 코치 선생님이야.
|
|
@@ -65,18 +63,18 @@ def generate_smart_system_prompt(grade_level):
|
|
| 65 |
|
| 66 |
학생이 SMART 기준에 맞춰 목표를 구체화하고, 그 목표를 달성하기 위한 실천 계획 (최소 3가지 구체적인 행동)까지 스스로 만들었다고 판단되면,
|
| 67 |
마지막에 학생이 직접 세운 내용을 명확하게 요약해서 보여줘.
|
| 68 |
-
예시: "정말 멋지다! 친구가 직접 세운 SMART 목표와 실천 계획을 함께 정리해볼까? \n\n**🎯 SMART 목표:** [학생이 정의한 구체적이고, 측정 가능하며, 달성 가능하고, 관련성 있고, 시간 제한이 있는 목표 요약]\n\n**👣 실천 계획:**\n1. [학생이 정의한 첫 번째 실천 단계]\n2. [학생이 정의한 두 번째 실천 단계]\n3. [학생이 정의한 세 번째 실천 단계]\n\n이렇게 계획을 세우니 목표가 훨씬 가까워진 느낌이지? 꾸준히 실천하면 꼭 이룰 수 있을 거야! 선생님이 응원할게! 😊"
|
| 69 |
-
요약하기 전에는 "이제 목표랑 계획이 다 세워진 것 같은데, 선생님이 한번 정리해
|
| 70 |
"""
|
| 71 |
else:
|
| 72 |
-
# 다른 학년 또는 기본 프롬프트
|
| 73 |
prompt = f"""
|
| 74 |
너는 {grade_level} 학생이 SMART 목표를 세우고 실천 계획을 만들도록 돕는 친절하고 격려하는 코치야.
|
| 75 |
학생이 이루고 싶은 목표나 상황을 이야기하면, 그 목표가 SMART(Specific, Measurable, Achievable, Relevant, Time-bound) 기준에 맞도록 질문을 통해 안내해줘.
|
| 76 |
절대 직접적인 제안이나 답을 주지 말고, 학생 스스로 생각하고 답을 찾도록 유도해줘.
|
| 77 |
학생의 학년 수준에 맞는 언어를 사용하고, 한 번에 한 가지씩 질문하며 대화를 이끌어가줘.
|
| 78 |
학생이 SMART 목표와 실천 계획을 모두 수립했다고 판단되면, 마지막에 학생이 만든 내용을 요약해서 정리해주고 격려해줘.
|
| 79 |
-
요약 전에는 학생에게 먼저 정리해도 될지 물어봐줘.
|
| 80 |
"""
|
| 81 |
return prompt
|
| 82 |
|
|
@@ -86,32 +84,40 @@ def openai_chat(grade_level):
|
|
| 86 |
# 현재 선택된 학년 수준에 맞는 시스템 프롬프트 생성
|
| 87 |
system_prompt = generate_smart_system_prompt(grade_level)
|
| 88 |
|
| 89 |
-
# 세션 상태의 첫 번째 메시지가 시스템 메시지인지 확인하고 업데이트
|
| 90 |
if not st.session_state.messages or st.session_state.messages[0]["role"] != "system":
|
| 91 |
-
# 시스템 메시지가 없
|
| 92 |
st.session_state.messages.insert(0, {"role": "system", "content": system_prompt})
|
| 93 |
else:
|
| 94 |
# 기존 시스템 메시지 내용 업데이트
|
| 95 |
st.session_state.messages[0]["content"] = system_prompt
|
| 96 |
|
|
|
|
| 97 |
response = openai.ChatCompletion.create(
|
| 98 |
-
model="gpt-4o",
|
| 99 |
-
messages=st.session_state.messages,
|
| 100 |
-
temperature=0.7,
|
| 101 |
-
max_tokens=1000, #
|
| 102 |
top_p=0.9,
|
| 103 |
-
frequency_penalty=0.1,
|
| 104 |
-
presence_penalty=0.1
|
| 105 |
)
|
| 106 |
return response.choices[0].message["content"]
|
| 107 |
-
except
|
| 108 |
-
st.error(f"API
|
| 109 |
-
return None
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
# --- Streamlit 앱 UI 설정 ---
|
| 112 |
-
st.set_page_config(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
# 페이지 스타일
|
| 115 |
st.markdown(
|
| 116 |
"""
|
| 117 |
<style>
|
|
@@ -121,53 +127,65 @@ st.markdown(
|
|
| 121 |
}
|
| 122 |
/* 타이틀 스타일 */
|
| 123 |
.main-title {
|
| 124 |
-
font-size: 2.5rem;
|
| 125 |
color: #4682b4; /* 차분한 파란색 */
|
| 126 |
font-weight: 700;
|
| 127 |
text-align: center;
|
| 128 |
margin-bottom: 20px;
|
| 129 |
}
|
| 130 |
-
/* 채팅 메시지
|
| 131 |
.chat-message {
|
| 132 |
border-radius: 15px;
|
| 133 |
-
padding: 15px;
|
| 134 |
margin: 10px 0;
|
| 135 |
display: flex;
|
| 136 |
align-items: flex-start; /* 아이콘과 텍스트 상단 정렬 */
|
| 137 |
-
flex-wrap: nowrap;
|
| 138 |
word-break: break-word;
|
| 139 |
-
max-width: 85%; /* 메시지 최대 너비
|
|
|
|
| 140 |
}
|
|
|
|
| 141 |
.chat-message-user {
|
| 142 |
background-color: #e0f7fa; /* 밝은 청록색 */
|
| 143 |
color: #00796b; /* 어두운 청록색 */
|
| 144 |
margin-left: auto; /* 오른쪽 정��� */
|
| 145 |
-
flex-direction: row-reverse; /* 아이콘
|
| 146 |
}
|
|
|
|
| 147 |
.chat-message-assistant {
|
| 148 |
-
background-color: #fff0f5; /* 라벤더 블러시
|
| 149 |
color: #c71585; /* 미디엄 바이올렛 레드 */
|
| 150 |
margin-right: auto; /* 왼쪽 정렬 */
|
| 151 |
-
flex-direction: row; /* 아이콘
|
| 152 |
}
|
|
|
|
| 153 |
.chat-avatar {
|
| 154 |
width: 40px;
|
| 155 |
height: 40px;
|
| 156 |
border-radius: 50%;
|
| 157 |
-
margin-right: 10px;
|
| 158 |
flex-shrink: 0; /* 아이콘 크기 고정 */
|
| 159 |
}
|
|
|
|
| 160 |
.chat-avatar-user {
|
| 161 |
-
margin-left: 10px;
|
| 162 |
margin-right: 0;
|
| 163 |
}
|
|
|
|
| 164 |
.chat-content {
|
| 165 |
flex-grow: 1; /* 텍스트 영역이 남은 공간 차지 */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
/* 사용자 입력 창 스타일 */
|
| 168 |
.stTextInput input {
|
| 169 |
border-radius: 15px;
|
| 170 |
border: 2px solid #add8e6; /* 밝은 파란색 */
|
|
|
|
| 171 |
}
|
| 172 |
/* 버튼 스타일 */
|
| 173 |
.stButton button {
|
|
@@ -175,6 +193,14 @@ st.markdown(
|
|
| 175 |
color: #fff;
|
| 176 |
border-radius: 15px;
|
| 177 |
padding: 10px 20px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
</style>
|
| 180 |
""",
|
|
@@ -187,124 +213,105 @@ st.markdown("<div class='main-title'>🎯 SMART 목표 설정 도우미 ✍️</
|
|
| 187 |
# --- 사이드바 설정 ---
|
| 188 |
with st.sidebar:
|
| 189 |
st.header("⚙️ 설정")
|
| 190 |
-
# 학년 수준 선택
|
| 191 |
grade_level_options = [
|
| 192 |
"초등학교 1학년", "초등학교 2학년", "초등학교 3학년", "초등학교 4학년", "초등학교 5학년", "초등학교 6학년",
|
| 193 |
"중학교 1학년", "중학교 2학년", "중학교 3학년",
|
| 194 |
"고등학교 1학년", "고등학교 2학년", "고등학교 3학년"
|
| 195 |
]
|
| 196 |
-
# 초등학교 6학년 인덱스 찾기
|
| 197 |
try:
|
| 198 |
default_index = grade_level_options.index("초등학교 6학년")
|
| 199 |
except ValueError:
|
| 200 |
-
default_index =
|
| 201 |
|
| 202 |
-
|
|
|
|
| 203 |
"👤 학생의 학년을 선택하세요:",
|
| 204 |
grade_level_options,
|
| 205 |
-
index=default_index
|
|
|
|
| 206 |
)
|
| 207 |
|
| 208 |
# 초기화 버튼
|
| 209 |
if st.button("🔄 대화 초기화"):
|
| 210 |
-
# 메시지 기록 삭제
|
| 211 |
st.session_state.messages = []
|
| 212 |
st.success("대화 내용이 초기화되었습니다. 새로운 목표를 설정해보세요!")
|
| 213 |
# 페이지 새로고침 없이 즉시 적용되도록 rerun 사용
|
| 214 |
st.rerun()
|
| 215 |
|
| 216 |
st.info("💡 AI 코치가 질문을 통해 스스로 SMART 목표와 실천 계획을 세우도록 도와줄 거예요!")
|
| 217 |
-
|
|
|
|
| 218 |
|
| 219 |
# --- 채팅 로직 ---
|
| 220 |
|
| 221 |
-
# 채팅 세션 초기화 (
|
| 222 |
if "messages" not in st.session_state:
|
| 223 |
st.session_state.messages = []
|
| 224 |
-
# 필요하다면 여기에 초기 환영 메시지 추가 가능
|
| 225 |
-
# st.session_state.messages.append({"role": "assistant", "content": "안녕하세요! 어떤 목표를 세우고 싶나요? 함께 이야기해봐요!"})
|
| 226 |
-
|
| 227 |
|
| 228 |
-
# 사용자와 AI
|
| 229 |
user_icon_url = "https://cdn-icons-png.flaticon.com/512/1995/1995531.png" # 학생 아이콘
|
| 230 |
assistant_icon_url = "https://cdn-icons-png.flaticon.com/512/4323/4323008.png" # 튜터 아이콘
|
| 231 |
|
| 232 |
-
#
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
st.markdown(
|
| 243 |
f"""
|
| 244 |
-
<div class='chat-message {
|
| 245 |
-
|
| 246 |
-
<img src='{
|
| 247 |
-
{'<div class="chat-content">' + message['content'] + '</div>' if message["role"] == "user" else ""}
|
| 248 |
</div>
|
| 249 |
""",
|
| 250 |
unsafe_allow_html=True
|
| 251 |
)
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
# 사용자 입력 받기
|
| 255 |
-
if prompt := st.chat_input("🎯 이루고 싶은 목표나 하고 싶은 일을 적어보세요! (예: 수학 시험 잘 보기)"):
|
| 256 |
-
# 사용자의 메시지를 세션에 추가
|
| 257 |
-
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 258 |
-
|
| 259 |
-
# 사용자 메시지 즉시 화면에 표시
|
| 260 |
-
st.markdown(
|
| 261 |
-
f"""
|
| 262 |
-
<div class='chat-message chat-message-user'>
|
| 263 |
-
<img src='{user_icon_url}' class='chat-avatar chat-avatar-user'>
|
| 264 |
-
<div class="chat-content">{prompt}</div>
|
| 265 |
-
</div>
|
| 266 |
-
""",
|
| 267 |
-
unsafe_allow_html=True
|
| 268 |
-
)
|
| 269 |
-
|
| 270 |
-
# AI 응답 생성 및 표시
|
| 271 |
-
with st.spinner("AI 코치가 생각 중이에요... 🤔"):
|
| 272 |
-
# OpenAI API 호출 (이제 grade_level만 전달)
|
| 273 |
-
response = openai_chat(grade_level)
|
| 274 |
-
|
| 275 |
-
if response: # 응답이 성공적으로 생성된 경우
|
| 276 |
-
st.session_state.messages.append({"role": "assistant", "content": response})
|
| 277 |
-
# AI 응답 화면에 표시
|
| 278 |
st.markdown(
|
| 279 |
f"""
|
| 280 |
-
<div class='chat-message chat-message-assistant'>
|
| 281 |
<img src='{assistant_icon_url}' class='chat-avatar'>
|
| 282 |
-
<div class="chat-content">{
|
| 283 |
</div>
|
| 284 |
""",
|
| 285 |
unsafe_allow_html=True
|
| 286 |
)
|
| 287 |
-
else:
|
| 288 |
-
# API 호출 실패 시, 사용자에게 알림 (오류 메시지는 openai_chat 내부에서 st.error로 표시됨)
|
| 289 |
-
# 실패한 사용자 메시지를 롤백할 수도 있음 (선택 사항)
|
| 290 |
-
# st.session_state.messages.pop()
|
| 291 |
-
pass
|
| 292 |
|
| 293 |
-
#
|
| 294 |
-
if
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
#
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
unsafe_allow_html=True
|
| 310 |
-
)
|
|
|
|
| 1 |
import os
|
| 2 |
import openai
|
| 3 |
import streamlit as st
|
| 4 |
+
# import html # 필요 시 주석 해제
|
| 5 |
|
| 6 |
+
# --- OpenAI API 설정 ---
|
| 7 |
# Streamlit Cloud secrets에서 API 키 가져오기
|
| 8 |
+
# 로컬 테스트 시 아래 줄을 주석 처리하고 os.getenv 부분을 활성화하세요.
|
| 9 |
openai_api_key = st.secrets["OPENAI_API_KEY"]
|
| 10 |
# 로컬 환경 변수에서 API 키 가져오기 (선택 사항)
|
| 11 |
# openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
|
|
| 13 |
# API 키가 있는지 확인
|
| 14 |
if not openai_api_key:
|
| 15 |
st.error("OpenAI API 키가 설정되지 않았습니다. 환경 변수나 Streamlit secrets에 키를 추가해주세요.")
|
| 16 |
+
st.stop() # API 키 없으면 앱 중지
|
| 17 |
|
| 18 |
openai.api_key = openai_api_key
|
| 19 |
|
| 20 |
+
# --- 함수 정의 ---
|
| 21 |
+
|
| 22 |
def generate_smart_system_prompt(grade_level):
|
| 23 |
"""학년 수준에 맞는 SMART 목표 설정 시스템 프롬프트를 생성합니다."""
|
| 24 |
+
|
| 25 |
+
# 학년 정보 추출 (예: "초등학교 6학년" -> "6", "초등학교")
|
| 26 |
try:
|
| 27 |
+
parts = grade_level.split(" ")
|
| 28 |
+
if len(parts) == 2:
|
| 29 |
+
school_level = parts[0]
|
| 30 |
+
grade_num = parts[1].rstrip("학년")
|
| 31 |
+
if school_level not in ["초등학교", "중학교", "고등학교"] or not grade_num.isdigit():
|
| 32 |
+
grade_num, school_level = "알 수 없음", "알 수 없음"
|
|
|
|
|
|
|
|
|
|
| 33 |
else:
|
| 34 |
+
grade_num, school_level = "알 수 없음", "알 수 없음"
|
| 35 |
+
except Exception: # 광범위한 예외 처리 (분석 실패 시)
|
| 36 |
+
grade_num, school_level = "알 수 없음", "알 수 없음"
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
# 초등학교 6학년 맞춤 프롬프트
|
|
|
|
| 39 |
if school_level == "초등학교" and grade_num == "6":
|
| 40 |
prompt = f"""
|
| 41 |
너는 초등학교 6학년 학생이 SMART 목표를 세우고 실천 계획을 만들도록 돕는 친절하고 격려하는 코치 선생님이야.
|
|
|
|
| 63 |
|
| 64 |
학생이 SMART 기준에 맞춰 목표를 구체화하고, 그 목표를 달성하기 위한 실천 계획 (최소 3가지 구체적인 행동)까지 스스로 만들었다고 판단되면,
|
| 65 |
마지막에 학생이 직접 세운 내용을 명확하게 요약해서 보여줘.
|
| 66 |
+
요약 예시: "정말 멋지다! 친구가 직접 세운 SMART 목표와 실천 계획을 함께 정리해볼까? \\n\\n**🎯 SMART 목표:** [학생이 정의한 구체적이고, 측정 가능하며, 달성 가능하고, 관련성 있고, 시간 제한이 있는 목표 요약]\\n\\n**👣 실천 계획:**\\n1. [학생이 정의한 첫 번째 실천 단계]\\n2. [학생이 정의한 두 번째 실천 단계]\\n3. [학생이 정의한 세 번째 실천 단계]\\n\\n이렇게 계획을 세우니 목표가 훨씬 가까워진 느낌이지? 꾸준히 실천하면 꼭 이룰 수 있을 거야! 선생님이 응원할게! 😊"
|
| 67 |
+
요약하기 전에는 반드시 "이제 목표랑 실천 계획이 다 세워진 것 같은데, 선생님이 한번 정리해봐도 괜찮을까?" 와 같이 학생의 동의를 구하는 질문을 먼저 해줘.
|
| 68 |
"""
|
| 69 |
else:
|
| 70 |
+
# 다른 학년 또는 기본 프롬프트
|
| 71 |
prompt = f"""
|
| 72 |
너는 {grade_level} 학생이 SMART 목표를 세우고 실천 계획을 만들도록 돕는 친절하고 격려하는 코치야.
|
| 73 |
학생이 이루고 싶은 목표나 상황을 이야기하면, 그 목표가 SMART(Specific, Measurable, Achievable, Relevant, Time-bound) 기준에 맞도록 질문을 통해 안내해줘.
|
| 74 |
절대 직접적인 제안이나 답을 주지 말고, 학생 스스로 생각하고 답을 찾도록 유도해줘.
|
| 75 |
학생의 학년 수준에 맞는 언어를 사용하고, 한 번에 한 가지씩 질문하며 대화를 이끌어가줘.
|
| 76 |
학생이 SMART 목표와 실천 계획을 모두 수립했다고 판단되면, 마지막에 학생이 만든 내용을 요약해서 정리해주고 격려해줘.
|
| 77 |
+
요약 전에는 반드시 "이제 목표와 계획이 잘 세워진 것 같네요. 제가 한번 정리해볼까요?" 와 같이 학생에게 먼저 정리해도 될지 물어봐줘.
|
| 78 |
"""
|
| 79 |
return prompt
|
| 80 |
|
|
|
|
| 84 |
# 현재 선택된 학년 수준에 맞는 시스템 프롬프트 생성
|
| 85 |
system_prompt = generate_smart_system_prompt(grade_level)
|
| 86 |
|
| 87 |
+
# 세션 상태의 첫 번째 메시지가 시스템 메시지인지 확인하고 업데이트/삽입
|
| 88 |
if not st.session_state.messages or st.session_state.messages[0]["role"] != "system":
|
| 89 |
+
# 시스템 메시지가 없거나 첫번째가 아니면 맨 앞에 삽입
|
| 90 |
st.session_state.messages.insert(0, {"role": "system", "content": system_prompt})
|
| 91 |
else:
|
| 92 |
# 기존 시스템 메시지 내용 업데이트
|
| 93 |
st.session_state.messages[0]["content"] = system_prompt
|
| 94 |
|
| 95 |
+
# API 호출 시 시스템 메시지를 포함한 전체 대화 전달
|
| 96 |
response = openai.ChatCompletion.create(
|
| 97 |
+
model="gpt-4o", # 또는 사용 가능한 최신 모델
|
| 98 |
+
messages=st.session_state.messages,
|
| 99 |
+
temperature=0.7,
|
| 100 |
+
max_tokens=1000, # 필요에 따라 조절
|
| 101 |
top_p=0.9,
|
| 102 |
+
frequency_penalty=0.1,
|
| 103 |
+
presence_penalty=0.1
|
| 104 |
)
|
| 105 |
return response.choices[0].message["content"]
|
| 106 |
+
except openai.error.OpenAIError as e: # 구체적인 OpenAI 에러 처리
|
| 107 |
+
st.error(f"OpenAI API 오류 발생: {str(e)}")
|
| 108 |
+
return None
|
| 109 |
+
except Exception as e: # 기타 예외 처리
|
| 110 |
+
st.error(f"알 수 없는 오류 발생: {str(e)}")
|
| 111 |
+
return None
|
| 112 |
|
| 113 |
# --- Streamlit 앱 UI 설정 ---
|
| 114 |
+
st.set_page_config(
|
| 115 |
+
page_title="SMART 목표 설정 도우미",
|
| 116 |
+
page_icon="🎯",
|
| 117 |
+
initial_sidebar_state="expanded"
|
| 118 |
+
)
|
| 119 |
|
| 120 |
+
# --- 페이지 스타일 (CSS) ---
|
| 121 |
st.markdown(
|
| 122 |
"""
|
| 123 |
<style>
|
|
|
|
| 127 |
}
|
| 128 |
/* 타이틀 스타일 */
|
| 129 |
.main-title {
|
| 130 |
+
font-size: 2.5rem;
|
| 131 |
color: #4682b4; /* 차분한 파란색 */
|
| 132 |
font-weight: 700;
|
| 133 |
text-align: center;
|
| 134 |
margin-bottom: 20px;
|
| 135 |
}
|
| 136 |
+
/* 채팅 메시지 컨테이너 */
|
| 137 |
.chat-message {
|
| 138 |
border-radius: 15px;
|
| 139 |
+
padding: 12px 15px; /* 패딩 약간 조정 */
|
| 140 |
margin: 10px 0;
|
| 141 |
display: flex;
|
| 142 |
align-items: flex-start; /* 아이콘과 텍스트 상단 정렬 */
|
| 143 |
+
flex-wrap: nowrap;
|
| 144 |
word-break: break-word;
|
| 145 |
+
max-width: 85%; /* 메시지 최대 너비 */
|
| 146 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05); /* 약간의 그림자 효과 */
|
| 147 |
}
|
| 148 |
+
/* 사용자 메시지 스타일 */
|
| 149 |
.chat-message-user {
|
| 150 |
background-color: #e0f7fa; /* 밝은 청록색 */
|
| 151 |
color: #00796b; /* 어두운 청록색 */
|
| 152 |
margin-left: auto; /* 오른쪽 정��� */
|
| 153 |
+
flex-direction: row-reverse; /* 내용과 아이콘 순서 변경 */
|
| 154 |
}
|
| 155 |
+
/* AI 메시지 스타일 */
|
| 156 |
.chat-message-assistant {
|
| 157 |
+
background-color: #fff0f5; /* 라벤더 블러시 */
|
| 158 |
color: #c71585; /* 미디엄 바이올렛 레드 */
|
| 159 |
margin-right: auto; /* 왼쪽 정렬 */
|
| 160 |
+
flex-direction: row; /* 기본 순서 (아이콘 먼저) */
|
| 161 |
}
|
| 162 |
+
/* 아바타(아이콘) 스타일 */
|
| 163 |
.chat-avatar {
|
| 164 |
width: 40px;
|
| 165 |
height: 40px;
|
| 166 |
border-radius: 50%;
|
| 167 |
+
margin-right: 10px; /* AI 메시지 아이콘 오른쪽 여백 */
|
| 168 |
flex-shrink: 0; /* 아이콘 크기 고정 */
|
| 169 |
}
|
| 170 |
+
/* 사용자 아바타 스타일 */
|
| 171 |
.chat-avatar-user {
|
| 172 |
+
margin-left: 10px; /* 사용자 메시지 아이콘 왼쪽 여백 */
|
| 173 |
margin-right: 0;
|
| 174 |
}
|
| 175 |
+
/* 채팅 내용 스타일 */
|
| 176 |
.chat-content {
|
| 177 |
flex-grow: 1; /* 텍스트 영역이 남은 공간 차지 */
|
| 178 |
+
/* 텍스트 선택 가능하게 (기본값) */
|
| 179 |
+
user-select: text;
|
| 180 |
+
-webkit-user-select: text;
|
| 181 |
+
-moz-user-select: text;
|
| 182 |
+
-ms-user-select: text;
|
| 183 |
}
|
| 184 |
/* 사용자 입력 창 스타일 */
|
| 185 |
.stTextInput input {
|
| 186 |
border-radius: 15px;
|
| 187 |
border: 2px solid #add8e6; /* 밝은 파란색 */
|
| 188 |
+
padding: 10px 15px;
|
| 189 |
}
|
| 190 |
/* 버튼 스타일 */
|
| 191 |
.stButton button {
|
|
|
|
| 193 |
color: #fff;
|
| 194 |
border-radius: 15px;
|
| 195 |
padding: 10px 20px;
|
| 196 |
+
border: none;
|
| 197 |
+
transition: background-color 0.2s ease; /* 부드러운 색상 변경 효과 */
|
| 198 |
+
}
|
| 199 |
+
.stButton button:hover {
|
| 200 |
+
background-color: #5a9bd3; /* 호버 시 약간 밝게 */
|
| 201 |
+
}
|
| 202 |
+
.stButton button:active {
|
| 203 |
+
background-color: #3e74a0; /* 클릭 시 약간 어둡게 */
|
| 204 |
}
|
| 205 |
</style>
|
| 206 |
""",
|
|
|
|
| 213 |
# --- 사이드바 설정 ---
|
| 214 |
with st.sidebar:
|
| 215 |
st.header("⚙️ 설정")
|
| 216 |
+
# 학년 수준 선택
|
| 217 |
grade_level_options = [
|
| 218 |
"초등학교 1학년", "초등학교 2학년", "초등학교 3학년", "초등학교 4학년", "초등학교 5학년", "초등학교 6학년",
|
| 219 |
"중학교 1학년", "중학교 2학년", "중학교 3학년",
|
| 220 |
"고등학교 1학년", "고등학교 2학년", "고등학교 3학년"
|
| 221 |
]
|
| 222 |
+
# 초등학교 6학년 인덱스 찾기 (더 안전하게)
|
| 223 |
try:
|
| 224 |
default_index = grade_level_options.index("초등학교 6학년")
|
| 225 |
except ValueError:
|
| 226 |
+
default_index = 5 # 리스트에 없으면 6번째 항목(초6)으로 가정
|
| 227 |
|
| 228 |
+
# 학년 선택 selectbox - 선택 변경 시 시스템 프롬프트 업데이트 위해 콜백 추가 가능성 고려
|
| 229 |
+
selected_grade = st.selectbox(
|
| 230 |
"👤 학생의 학년을 선택하세요:",
|
| 231 |
grade_level_options,
|
| 232 |
+
index=default_index,
|
| 233 |
+
key="grade_select" # 키 추가
|
| 234 |
)
|
| 235 |
|
| 236 |
# 초기화 버튼
|
| 237 |
if st.button("🔄 대화 초기화"):
|
| 238 |
+
# 메시지 기록 삭제
|
| 239 |
st.session_state.messages = []
|
| 240 |
st.success("대화 내용이 초기화되었습니다. 새로운 목표를 설정해보세요!")
|
| 241 |
# 페이지 새로고침 없이 즉시 적용되도록 rerun 사용
|
| 242 |
st.rerun()
|
| 243 |
|
| 244 |
st.info("💡 AI 코치가 질문을 통해 스스로 SMART 목표와 실천 계획을 세우도록 도와줄 거예요!")
|
| 245 |
+
st.markdown("---") # 구분선
|
| 246 |
+
st.caption("Powered by OpenAI GPT-4o") # 모델 정보 등 추가 정보
|
| 247 |
|
| 248 |
# --- 채팅 로직 ---
|
| 249 |
|
| 250 |
+
# 채팅 세션 초기화 (메시지 리스트가 없으면 생성)
|
| 251 |
if "messages" not in st.session_state:
|
| 252 |
st.session_state.messages = []
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
+
# 사용자와 AI 아이콘 URL 설정
|
| 255 |
user_icon_url = "https://cdn-icons-png.flaticon.com/512/1995/1995531.png" # 학생 아이콘
|
| 256 |
assistant_icon_url = "https://cdn-icons-png.flaticon.com/512/4323/4323008.png" # 튜터 아이콘
|
| 257 |
|
| 258 |
+
# 초기 메시지 추가 (세션이 비어있을 때만 실행)
|
| 259 |
+
if not st.session_state.messages:
|
| 260 |
+
# 현재 선택된 학년으로 시스템 프롬프트 설정
|
| 261 |
+
system_prompt = generate_smart_system_prompt(selected_grade) # 사이드바에서 선택된 값 사용
|
| 262 |
+
st.session_state.messages.append({"role": "system", "content": system_prompt})
|
| 263 |
+
# 초기 환영 메시지 추가
|
| 264 |
+
welcome_message = "안녕! 👋 나는 네 목표 설정을 도와줄 AI 코치 선생님이야. 이루고 싶은 목표나 하고 싶은 일이 있으면 나에게 이야기해 줄래? 같이 멋진 계획을 세워보자! 😊"
|
| 265 |
+
st.session_state.messages.append({"role": "assistant", "content": welcome_message})
|
| 266 |
+
# 초기 메시지는 아래 메시지 표시 루프에서 자동으로 그려짐. 여기서 st.rerun() 불필요.
|
| 267 |
+
|
| 268 |
+
# --- 채팅 메시지 표시 ---
|
| 269 |
+
# st.session_state.messages에 있는 모든 메시지를 순서대로 화면에 그림
|
| 270 |
+
for index, message in enumerate(st.session_state.messages):
|
| 271 |
+
if message["role"] == "system":
|
| 272 |
+
continue # 시스템 메시지는 건너뜀
|
| 273 |
|
| 274 |
+
role = message["role"]
|
| 275 |
+
content = message["content"]
|
| 276 |
+
# content = html.escape(message["content"]) # HTML 태그가 문제될 경우 주석 해제
|
| 277 |
+
|
| 278 |
+
# 역할에 따라 다른 스타일과 구조 적용
|
| 279 |
+
if role == "user":
|
| 280 |
+
# 사용자 메시지: [내용] [아이콘]
|
| 281 |
st.markdown(
|
| 282 |
f"""
|
| 283 |
+
<div class='chat-message chat-message-user' key='user_msg_{index}'>
|
| 284 |
+
<div class="chat-content">{content}</div>
|
| 285 |
+
<img src='{user_icon_url}' class='chat-avatar chat-avatar-user'>
|
|
|
|
| 286 |
</div>
|
| 287 |
""",
|
| 288 |
unsafe_allow_html=True
|
| 289 |
)
|
| 290 |
+
elif role == "assistant":
|
| 291 |
+
# AI 메시지: [아이콘] [내용]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
st.markdown(
|
| 293 |
f"""
|
| 294 |
+
<div class='chat-message chat-message-assistant' key='assistant_msg_{index}'>
|
| 295 |
<img src='{assistant_icon_url}' class='chat-avatar'>
|
| 296 |
+
<div class="chat-content">{content}</div>
|
| 297 |
</div>
|
| 298 |
""",
|
| 299 |
unsafe_allow_html=True
|
| 300 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
+
# --- 사용자 입력 처리 ---
|
| 303 |
+
if prompt := st.chat_input("🎯 이루고 싶은 목표나 하고 싶은 일을 적어보세요! (예: 수학 시험 잘 보기)"):
|
| 304 |
+
# 1. 사용자 메시지를 세션 상태에 추가
|
| 305 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 306 |
+
|
| 307 |
+
# 2. AI 응답 생성 (스피너 표시)
|
| 308 |
+
with st.spinner("AI 코치가 생각 중이에요... 🤔"):
|
| 309 |
+
response = openai_chat(selected_grade) # 사이드바에서 선택된 학년 정보 전달
|
| 310 |
+
|
| 311 |
+
# 3. AI 응답이 성공적이면 세션 상태에 추가
|
| 312 |
+
if response:
|
| 313 |
+
st.session_state.messages.append({"role": "assistant", "content": response})
|
| 314 |
+
# API 호출 실패 시 openai_chat 함수 내에서 st.error가 호출됨
|
| 315 |
+
|
| 316 |
+
# 4. 페이지를 다시 로드하여 새 메시지를 포함한 전체 대화 내용을 그림
|
| 317 |
+
st.rerun()
|
|
|
|
|
|