ll7098ll commited on
Commit
680a728
·
verified ·
1 Parent(s): ff9b005

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +130 -123
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
- if grade_level.startswith("초등학교"):
24
- grade_num = grade_level.split(" ")[1].rstrip("학년")
25
- school_level = "초등학교"
26
- elif grade_level.startswith(""):
27
- grade_num = grade_level.split(" ")[1].rstrip("학")
28
- school_level = "중학교"
29
- elif grade_level.startswith("고등학교"):
30
- grade_num = grade_level.split(" ")[1].rstrip("학년")
31
- school_level = "고등학교"
32
  else:
33
- grade_num = "알 수 없음"
34
- school_level = "알 없음"
35
- except IndexError:
36
- grade_num = "알 수 없음"
37
- school_level = "알 수 없음"
38
 
39
- # 초등학교 6학년 특화된, 그리고 SMART 목표 설정에 초점을 프롬프트
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
- # 다른 학년 또는 기본 프롬프트 (SMART 목표 설정으로 유지하되, 언어 수준 조정 가능)
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, # 응답 길이 조절 (SMART 목표 설정 대화에 충분하도록)
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 Exception as e:
108
- st.error(f"API 호출 중 오류 발생: {str(e)}")
109
- return None # 오류 발생 시 None 반환
 
 
 
110
 
111
  # --- Streamlit 앱 UI 설정 ---
112
- st.set_page_config(page_title="SMART 목표 설정 도우미", page_icon="🎯", initial_sidebar_state="expanded")
 
 
 
 
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
- # 학년 수준 선택 (초등학교 6학년 기본 선택)
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 = 0 # 혹시 목록에 없으면 번째 항목 선택
201
 
202
- grade_level = st.selectbox(
 
203
  "👤 학생의 학년을 선택하세요:",
204
  grade_level_options,
205
- index=default_index # 초등학교 6학년을 기본값으로 설정
 
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
- # 채팅 세션 초기화 (기본 시스템 메시지 없이 시작, openai_chat에서 동적 추가)
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 아이콘 URL 설정
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
- for message in st.session_state.messages:
235
- if message["role"] != "system":
236
- role_class = "chat-message-user" if message["role"] == "user" else "chat-message-assistant"
237
- avatar_url = user_icon_url if message["role"] == "user" else assistant_icon_url
238
- avatar_class = "chat-avatar-user" if message["role"] == "user" else "chat-avatar"
239
- align_class = "chat-message-user" if message["role"] == "user" else "chat-message-assistant"
 
 
 
 
 
 
 
240
 
241
- # HTML 구조 수정하여 아이콘과 내용 분리
 
 
 
 
 
 
242
  st.markdown(
243
  f"""
244
- <div class='chat-message {align_class}'>
245
- {'<div class="chat-content">' + message['content'] + '</div>' if message["role"] == "assistant" else ""}
246
- <img src='{avatar_url}' class='chat-avatar {avatar_class}'>
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">{response}</div>
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 not st.session_state.messages:
295
- # 초기스템 프롬프트 설정 (필수)
296
- system_prompt = generate_smart_system_prompt(grade_level)
297
- st.session_state.messages.append({"role": "system", "content": system_prompt})
298
- # 초기 ��영 메시지 추가
299
- welcome_message = "안녕! 👋 나는 네 목표 설정을 도와줄 AI 코치 님이야.루고 싶은 목표나 하고 싶은 일이 있으면 나 이야기해 줄래? 같이 멋진 계획을 세워보자! 😊"
300
- st.session_state.messages.append({"role": "assistant", "content": welcome_message})
301
- # 환영 메시지 표시
302
- st.markdown(
303
- f"""
304
- <div class='chat-message chat-message-assistant'>
305
- <img src='{assistant_icon_url}' class='chat-avatar'>
306
- <div class="chat-content">{welcome_message}</div>
307
- </div>
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()