File size: 15,025 Bytes
1d355ad
2ab8dd5
e90cb1f
680a728
d1b4f6b
680a728
4ab59f5
 
 
ff9b005
 
 
680a728
ff9b005
2ab8dd5
d1b4f6b
680a728
 
ff9b005
f29cb4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13ca125
4ab59f5
13ca125
 
 
 
 
 
 
 
 
f29cb4f
13ca125
 
 
 
 
 
 
 
f29cb4f
13ca125
 
 
f29cb4f
13ca125
 
 
 
 
ff9b005
 
 
 
 
 
 
9a41ff4
680a728
ff9b005
680a728
ff9b005
f29cb4f
 
ff9b005
4ab59f5
f29cb4f
 
680a728
f29cb4f
680a728
4ab59f5
2ab8dd5
680a728
 
beb811e
f29cb4f
 
680a728
 
 
 
 
5ff42d5
ff9b005
680a728
 
 
 
 
5ff42d5
680a728
2ab8dd5
 
 
 
 
ff9b005
2ab8dd5
 
 
680a728
ff9b005
2ab8dd5
 
 
 
680a728
2ab8dd5
 
680a728
2ab8dd5
 
ff9b005
680a728
2ab8dd5
680a728
 
2ab8dd5
680a728
2ab8dd5
ff9b005
 
 
680a728
2ab8dd5
680a728
2ab8dd5
680a728
ff9b005
 
680a728
2ab8dd5
680a728
2ab8dd5
 
 
 
680a728
ff9b005
2ab8dd5
680a728
2ab8dd5
680a728
2ab8dd5
 
680a728
ff9b005
 
680a728
 
 
 
 
ff9b005
2ab8dd5
 
 
ff9b005
680a728
2ab8dd5
 
 
ff9b005
2ab8dd5
 
 
680a728
 
 
 
 
 
 
 
2ab8dd5
 
 
 
 
5ff42d5
2ab8dd5
ff9b005
2ab8dd5
ff9b005
2ab8dd5
ff9b005
680a728
9a41ff4
 
 
f29cb4f
9a41ff4
f29cb4f
ff9b005
 
 
f29cb4f
ff9b005
f29cb4f
680a728
f29cb4f
ff9b005
680a728
13ca125
ff9b005
 
2ab8dd5
ff9b005
f29cb4f
ff9b005
 
 
2ab8dd5
 
ff9b005
680a728
 
ff9b005
 
 
680a728
ff9b005
 
 
680a728
ff9b005
 
 
f29cb4f
 
 
 
 
 
13ca125
f29cb4f
 
680a728
 
 
 
 
f29cb4f
ff9b005
680a728
 
f29cb4f
680a728
 
 
f29cb4f
06c54b2
ff9b005
680a728
 
 
ff9b005
 
2ab8dd5
06c54b2
680a728
f29cb4f
ff9b005
 
680a728
ff9b005
680a728
ff9b005
 
 
 
 
680a728
 
 
 
 
 
 
f29cb4f
680a728
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import os
import openai
import streamlit as st
# import html # 필요 시 주석 해제

# --- OpenAI API 설정 ---
openai_api_key = st.secrets["OPENAI_API_KEY"]


# API 키가 있는지 확인
if not openai_api_key:
    st.error("OpenAI API 키가 설정되지 않았습니다. 환경 변수나 Streamlit secrets에 키를 추가해주세요.")
    st.stop() # API 키 없으면 앱 중지

openai.api_key = openai_api_key

# --- 함수 정의 ---

def generate_smart_system_prompt(grade_level):
    """학년 수준에 맞는 SMART 목표 설정 시스템 프롬프트를 생성합니다."""

    # 학년 정보 추출 (예: "초등학교 6학년" -> "6", "초등학교")
    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 = "알 수 없음", "알 수 없음"

    # 6학년 프롬프트 기준으로 통합
    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

        # API 호출 시 시스템 메시지를 포함한 전체 대화 전달
        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: # 구체적인 OpenAI 에러 처리
        st.error(f"OpenAI API 오류 발생: {str(e)}")
        return None
    except Exception as e: # 기타 예외 처리
        st.error(f"알 수 없는 오류 발생: {str(e)}")
        return None

# --- Streamlit 앱 UI 설정 ---
st.set_page_config(
    page_title="SMART 목표 설정 도우미",
    page_icon="🎯",
    initial_sidebar_state="expanded"
)

# --- 페이지 스타일 (CSS) ---
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학년"
    ]
    # 초등학교 6학년 인덱스 찾기 (더 안전하게)
    try:
        default_index = grade_level_options.index("초등학교 6학년")
    except ValueError:
        default_index = 5 # 리스트에 없으면 6번째 항목(초6)으로 가정

    # 학년 선택 selectbox - 선택 변경 시 시스템 프롬프트 업데이트 위해 콜백 추가 가능성 고려
    selected_grade = st.selectbox(
        "👤 학생의 학년을 선택하세요:",
        grade_level_options,
        index=default_index,
        key="grade_select" # 키 추가
    )

    # 초기화 버튼
    if st.button("🔄 대화 초기화"):
        # 메시지 기록 삭제
        st.session_state.messages = []
        st.success("대화 내용이 초기화되었습니다. 새로운 목표를 설정해보세요!")
        # 페이지 새로고침 없이 즉시 적용되도록 rerun 사용
        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 = []

# 사용자와 AI 아이콘 URL 설정
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})
    # 초기 메시지는 아래 메시지 표시 루프에서 자동으로 그려짐. 여기서 st.rerun() 불필요.

# --- 채팅 메시지 표시 ---
# st.session_state.messages에 있는 모든 메시지를 순서대로 화면에 그림
for index, message in enumerate(st.session_state.messages):
    if message["role"] == "system":
        continue # 시스템 메시지는 건너뜀

    role = message["role"]
    content = message["content"]
    # content = html.escape(message["content"]) # HTML 태그가 문제될 경우 주석 해제

    # 역할에 따라 다른 스타일과 구조 적용
    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":
        # AI 메시지: [아이콘] [내용]
        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("🎯 이루고 싶은 목표나 하고 싶은 일을 적어보세요! (예: 수학 시험 잘 보기)"):
    # 1. 사용자 메시지를 세션 상태에 추가
    st.session_state.messages.append({"role": "user", "content": prompt})

    # 2. AI 응답 생성 (스피너 표시)
    with st.spinner("AI 코치가 생각 중이에요... 🤔"):
        response = openai_chat(selected_grade) # 사이드바에서 선택된 학년 정보 전달

    # 3. AI 응답이 성공적이면 세션 상태에 추가
    if response:
        st.session_state.messages.append({"role": "assistant", "content": response})
    # API 호출 실패 시 openai_chat 함수 내에서 st.error가 호출됨

    # 4. 페이지를 다시 로드하여 새 메시지를 포함한 전체 대화 내용을 그림
    st.rerun()