Rename app_claude.py to app_work.py
Browse files- app_claude.py +0 -725
- app_work.py +2212 -0
app_claude.py
DELETED
@@ -1,725 +0,0 @@
|
|
1 |
-
import streamlit as st
|
2 |
-
import openai
|
3 |
-
from datetime import datetime
|
4 |
-
import json
|
5 |
-
from collections import defaultdict
|
6 |
-
|
7 |
-
# Custom CSS for layout
|
8 |
-
st.markdown("""
|
9 |
-
<style>
|
10 |
-
.box-container {
|
11 |
-
background-color: #ffffff;
|
12 |
-
border-radius: 10px;
|
13 |
-
padding: 20px;
|
14 |
-
margin: 10px 0;
|
15 |
-
border: 2px solid #eee;
|
16 |
-
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
17 |
-
}
|
18 |
-
|
19 |
-
.story-box {
|
20 |
-
max-height: 300px;
|
21 |
-
overflow-y: auto;
|
22 |
-
padding: 15px;
|
23 |
-
background-color: #f8f9fa;
|
24 |
-
border-radius: 8px;
|
25 |
-
}
|
26 |
-
|
27 |
-
.input-box {
|
28 |
-
background-color: #ffffff;
|
29 |
-
padding: 15px;
|
30 |
-
border-radius: 8px;
|
31 |
-
}
|
32 |
-
|
33 |
-
.help-box {
|
34 |
-
background-color: #f0f7ff;
|
35 |
-
padding: 15px;
|
36 |
-
border-radius: 8px;
|
37 |
-
}
|
38 |
-
|
39 |
-
.rewards-box {
|
40 |
-
background-color: #fff9f0;
|
41 |
-
padding: 15px;
|
42 |
-
border-radius: 8px;
|
43 |
-
}
|
44 |
-
|
45 |
-
.stButton button {
|
46 |
-
width: 100%;
|
47 |
-
border-radius: 20px;
|
48 |
-
margin: 5px 0;
|
49 |
-
}
|
50 |
-
|
51 |
-
.story-text {
|
52 |
-
padding: 10px;
|
53 |
-
border-radius: 5px;
|
54 |
-
margin: 5px 0;
|
55 |
-
}
|
56 |
-
|
57 |
-
.ai-text {
|
58 |
-
background-color: #e3f2fd;
|
59 |
-
}
|
60 |
-
|
61 |
-
.player-text {
|
62 |
-
background-color: #f0f4c3;
|
63 |
-
}
|
64 |
-
|
65 |
-
.prompt-text {
|
66 |
-
color: #666;
|
67 |
-
font-style: italic;
|
68 |
-
margin: 5px 0;
|
69 |
-
}
|
70 |
-
|
71 |
-
.badge-container {
|
72 |
-
display: inline-block;
|
73 |
-
margin: 5px;
|
74 |
-
padding: 10px;
|
75 |
-
background-color: #ffd700;
|
76 |
-
border-radius: 50%;
|
77 |
-
text-align: center;
|
78 |
-
}
|
79 |
-
</style>
|
80 |
-
""", unsafe_allow_html=True)
|
81 |
-
|
82 |
-
# Initialize OpenAI
|
83 |
-
openai.api_key = st.secrets["OPENAI_API_KEY"]
|
84 |
-
|
85 |
-
# Initialize session state variables
|
86 |
-
if 'story' not in st.session_state:
|
87 |
-
st.session_state.story = []
|
88 |
-
if 'current_level' not in st.session_state:
|
89 |
-
st.session_state.current_level = 'beginner'
|
90 |
-
if 'story_started' not in st.session_state:
|
91 |
-
st.session_state.story_started = False
|
92 |
-
if 'first_turn' not in st.session_state:
|
93 |
-
st.session_state.first_turn = None
|
94 |
-
if 'current_turn' not in st.session_state:
|
95 |
-
st.session_state.current_turn = None
|
96 |
-
if 'badges' not in st.session_state:
|
97 |
-
st.session_state.badges = defaultdict(int)
|
98 |
-
if 'vocabulary_used' not in st.session_state:
|
99 |
-
st.session_state.vocabulary_used = set()
|
100 |
-
|
101 |
-
def get_ai_story_continuation(story_context, level):
|
102 |
-
"""Get story continuation from OpenAI API"""
|
103 |
-
try:
|
104 |
-
response = openai.ChatCompletion.create(
|
105 |
-
model="gpt-4o-mini",
|
106 |
-
messages=[
|
107 |
-
{"role": "system", "content": f"""You are a creative storyteller helping a {level} level English student write a story.
|
108 |
-
|
109 |
-
Rules:
|
110 |
-
1. READ the story context carefully
|
111 |
-
2. ADD meaningful story elements (new events, character development, dialogue)
|
112 |
-
3. AVOID generic transitions like 'suddenly' or 'then'
|
113 |
-
4. ENSURE story progression and character development
|
114 |
-
5. USE vocabulary appropriate for {level} level
|
115 |
-
|
116 |
-
Current story elements:
|
117 |
-
- Characters introduced: {extract_characters(story_context)}
|
118 |
-
- Current setting: {extract_setting(story_context)}
|
119 |
-
- Recent events: {extract_recent_events(story_context)}
|
120 |
-
|
121 |
-
Provide JSON response with:
|
122 |
-
1. continuation: Create an interesting next story event (1-2 sentences)
|
123 |
-
2. creative_prompt: Ask about specific details/choices for the next part
|
124 |
-
3. vocabulary_suggestions: 3 contextual words for the next part
|
125 |
-
4. translation: Thai translation of continuation
|
126 |
-
5. story_elements: New elements introduced (characters/places/objects)"""},
|
127 |
-
{"role": "user", "content": f"Story context: {story_context}\nContinue the story creatively and maintain narrative flow."}
|
128 |
-
],
|
129 |
-
temperature=0.7
|
130 |
-
)
|
131 |
-
|
132 |
-
result = json.loads(response.choices[0].message.content)
|
133 |
-
|
134 |
-
# Validate story continuation
|
135 |
-
if is_generic_response(result['continuation']):
|
136 |
-
return get_ai_story_continuation(story_context, level)
|
137 |
-
|
138 |
-
return result
|
139 |
-
|
140 |
-
except Exception as e:
|
141 |
-
st.error(f"Error with AI response: {e}")
|
142 |
-
return create_fallback_response(story_context)
|
143 |
-
|
144 |
-
def extract_characters(story_context):
|
145 |
-
"""Extract character information from story"""
|
146 |
-
try:
|
147 |
-
char_response = openai.ChatCompletion.create(
|
148 |
-
model="gpt-4o-mini",
|
149 |
-
messages=[
|
150 |
-
{"role": "system", "content": "List all characters mentioned in the story."},
|
151 |
-
{"role": "user", "content": story_context}
|
152 |
-
]
|
153 |
-
)
|
154 |
-
return char_response.choices[0].message.content
|
155 |
-
except:
|
156 |
-
return "Unknown characters"
|
157 |
-
|
158 |
-
def extract_setting(story_context):
|
159 |
-
"""Extract setting information from story"""
|
160 |
-
try:
|
161 |
-
setting_response = openai.ChatCompletion.create(
|
162 |
-
model="gpt-4o-mini",
|
163 |
-
messages=[
|
164 |
-
{"role": "system", "content": "Describe the current setting of the story."},
|
165 |
-
{"role": "user", "content": story_context}
|
166 |
-
]
|
167 |
-
)
|
168 |
-
return setting_response.choices[0].message.content
|
169 |
-
except:
|
170 |
-
return "Unknown setting"
|
171 |
-
|
172 |
-
def extract_recent_events(story_context):
|
173 |
-
"""Extract recent events from story"""
|
174 |
-
try:
|
175 |
-
events_response = openai.ChatCompletion.create(
|
176 |
-
model="gpt-4o-mini",
|
177 |
-
messages=[
|
178 |
-
{"role": "system", "content": "Summarize the most recent events in the story."},
|
179 |
-
{"role": "user", "content": story_context}
|
180 |
-
]
|
181 |
-
)
|
182 |
-
return events_response.choices[0].message.content
|
183 |
-
except:
|
184 |
-
return "Unknown events"
|
185 |
-
|
186 |
-
def is_generic_response(continuation):
|
187 |
-
"""Check if the response is too generic"""
|
188 |
-
generic_phrases = [
|
189 |
-
"suddenly", "then", "something happened",
|
190 |
-
"unexpectedly", "the story continues"
|
191 |
-
]
|
192 |
-
return any(phrase in continuation.lower() for phrase in generic_phrases)
|
193 |
-
|
194 |
-
def create_fallback_response(story_context):
|
195 |
-
"""Create contextual fallback response"""
|
196 |
-
# Extract last event or setting from story_context
|
197 |
-
last_event = story_context.split('.')[-2] if len(story_context.split('.')) > 1 else story_context
|
198 |
-
|
199 |
-
return {
|
200 |
-
"continuation": f"The characters decided to explore further into the story.",
|
201 |
-
"creative_prompt": "What interesting detail should we add to the scene?",
|
202 |
-
"vocabulary_suggestions": ["explore", "adventure", "discover"],
|
203 |
-
"translation": "ตัวละครตัดสินใจสำรวจเรื่องราวต่อไป",
|
204 |
-
"story_elements": {"new_elements": ["exploration"]}
|
205 |
-
}
|
206 |
-
|
207 |
-
def validate_story_quality(story_context, new_continuation):
|
208 |
-
"""Validate that the story maintains quality and coherence"""
|
209 |
-
try:
|
210 |
-
validation = openai.ChatCompletion.create(
|
211 |
-
model="gpt-4o-mini",
|
212 |
-
messages=[
|
213 |
-
{"role": "system", "content": """Validate story coherence and quality.
|
214 |
-
Return JSON with:
|
215 |
-
1. is_coherent: boolean
|
216 |
-
2. reason: string explanation if not coherent"""},
|
217 |
-
{"role": "user", "content": f"""Previous: {story_context}
|
218 |
-
New continuation: {new_continuation}
|
219 |
-
Is this a logical and non-repetitive continuation?"""}
|
220 |
-
]
|
221 |
-
)
|
222 |
-
|
223 |
-
result = json.loads(validation.choices[0].message.content)
|
224 |
-
return result['is_coherent'], result.get('reason', '')
|
225 |
-
|
226 |
-
except Exception:
|
227 |
-
return True, "" # Default to accepting if validation fails
|
228 |
-
|
229 |
-
def start_story(level):
|
230 |
-
"""Get the first sentence from AI"""
|
231 |
-
# Define level-specific starting prompts
|
232 |
-
level_prompts = {
|
233 |
-
'beginner': {
|
234 |
-
'guidance': "Start with a simple, engaging sentence about a person, animal, or place.",
|
235 |
-
'examples': "Example: 'A happy dog lived in a blue house.'"
|
236 |
-
},
|
237 |
-
'intermediate': {
|
238 |
-
'guidance': "Start with an interesting situation using compound sentences.",
|
239 |
-
'examples': "Example: 'On a sunny morning, Sarah found a mysterious letter in her garden.'"
|
240 |
-
},
|
241 |
-
'advanced': {
|
242 |
-
'guidance': "Start with a sophisticated hook using complex sentence structure.",
|
243 |
-
'examples': "Example: 'Deep in the ancient forest, where shadows danced beneath ancient trees, a peculiar sound echoed through the mist.'"
|
244 |
-
}
|
245 |
-
}
|
246 |
-
|
247 |
-
try:
|
248 |
-
response = openai.ChatCompletion.create(
|
249 |
-
model="gpt-4o-mini", # หรือ model ที่ถูกต้องตามที่คุณใช้
|
250 |
-
messages=[
|
251 |
-
{"role": "system", "content": f"""You are starting a story for a {level} level English student.
|
252 |
-
{level_prompts[level]['guidance']}
|
253 |
-
{level_prompts[level]['examples']}
|
254 |
-
|
255 |
-
Provide a JSON response with:
|
256 |
-
1. opening: An engaging opening sentence (matching the specified level)
|
257 |
-
2. creative_prompt: A short question (3-8 words) to guide the student's first sentence
|
258 |
-
3. vocabulary_suggestions: 3 words they might use in their response (appropriate for {level} level)
|
259 |
-
4. translation: Thai translation of the opening sentence
|
260 |
-
|
261 |
-
Make the opening engaging but appropriate for the student's level."""},
|
262 |
-
{"role": "user", "content": "Please start a new story."}
|
263 |
-
],
|
264 |
-
temperature=0.7
|
265 |
-
)
|
266 |
-
return json.loads(response.choices[0].message.content)
|
267 |
-
except Exception as e:
|
268 |
-
st.error(f"Error with AI response: {e}")
|
269 |
-
return {
|
270 |
-
"opening": "Once upon a time...",
|
271 |
-
"creative_prompt": "Who is our main character?",
|
272 |
-
"vocabulary_suggestions": ["brave", "curious", "friendly"],
|
273 |
-
"translation": "กาลครั้งหนึ่ง..."
|
274 |
-
}
|
275 |
-
|
276 |
-
def main():
|
277 |
-
st.title("🎨 Interactive Story Adventure")
|
278 |
-
|
279 |
-
# Initial setup (level selection and first turn choice)
|
280 |
-
if not st.session_state.story_started:
|
281 |
-
with st.container():
|
282 |
-
st.markdown('<div class="box-container">', unsafe_allow_html=True)
|
283 |
-
st.write("### Welcome to Story Adventure!")
|
284 |
-
col1, col2 = st.columns(2)
|
285 |
-
with col1:
|
286 |
-
level = st.selectbox(
|
287 |
-
"Choose your English level:",
|
288 |
-
['beginner', 'intermediate', 'advanced']
|
289 |
-
)
|
290 |
-
with col2:
|
291 |
-
first_turn = st.radio(
|
292 |
-
"Who should start the story?",
|
293 |
-
['AI', 'Player']
|
294 |
-
)
|
295 |
-
if st.button("Start Story!", key="start_button"):
|
296 |
-
st.session_state.current_level = level
|
297 |
-
st.session_state.first_turn = first_turn
|
298 |
-
st.session_state.current_turn = first_turn
|
299 |
-
st.session_state.story_started = True
|
300 |
-
if first_turn == 'AI':
|
301 |
-
response = start_story(level)
|
302 |
-
st.session_state.story.append({
|
303 |
-
'author': 'AI',
|
304 |
-
'text': response['opening'],
|
305 |
-
'prompt': response['creative_prompt'],
|
306 |
-
'vocabulary': response['vocabulary_suggestions'],
|
307 |
-
'timestamp': datetime.now().isoformat()
|
308 |
-
})
|
309 |
-
st.session_state.current_turn = 'Player'
|
310 |
-
st.rerun()
|
311 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
312 |
-
|
313 |
-
# Main story interface with 4-box layout
|
314 |
-
if st.session_state.story_started:
|
315 |
-
# Box 1: Story Display
|
316 |
-
with st.container():
|
317 |
-
st.markdown('<div class="box-container story-box">', unsafe_allow_html=True)
|
318 |
-
st.write("### 📖 Our Story So Far:")
|
319 |
-
|
320 |
-
for entry in st.session_state.story:
|
321 |
-
# แสดงเนื้อเรื่อง
|
322 |
-
css_class = "ai-text" if entry['author'] == 'AI' else "player-text"
|
323 |
-
st.markdown(f'<div class="story-text {css_class}">{entry["text"]}</div>',
|
324 |
-
unsafe_allow_html=True)
|
325 |
-
|
326 |
-
# ถ้าเป็น AI response
|
327 |
-
if entry['author'] == 'AI':
|
328 |
-
# แสดงคำแปล
|
329 |
-
if 'translation' in entry:
|
330 |
-
st.markdown(f'''
|
331 |
-
<div class="translation-box">
|
332 |
-
🇹🇭 {entry["translation"]}
|
333 |
-
</div>
|
334 |
-
''', unsafe_allow_html=True)
|
335 |
-
|
336 |
-
# แสดงคำถามกระตุ้นความคิด
|
337 |
-
if 'prompt' in entry:
|
338 |
-
st.markdown(f'''
|
339 |
-
<div class="prompt-box">
|
340 |
-
💭 {entry["prompt"]}
|
341 |
-
</div>
|
342 |
-
''', unsafe_allow_html=True)
|
343 |
-
|
344 |
-
# แสดงคำศัพท์แนะนำ
|
345 |
-
if 'vocabulary' in entry:
|
346 |
-
st.markdown(f'''
|
347 |
-
<div class="vocabulary-box">
|
348 |
-
📚 Try using: {', '.join(entry["vocabulary"])}
|
349 |
-
</div>
|
350 |
-
''', unsafe_allow_html=True)
|
351 |
-
|
352 |
-
# แสดงองค์ประกอบใหม่ในเรื่อง
|
353 |
-
if 'story_elements' in entry:
|
354 |
-
st.markdown(f'''
|
355 |
-
<div class="elements-box">
|
356 |
-
🎭 New elements: {', '.join(entry["story_elements"]["new_elements"])}
|
357 |
-
</div>
|
358 |
-
''', unsafe_allow_html=True)
|
359 |
-
|
360 |
-
# Box 2: Writing Area
|
361 |
-
if st.session_state.current_turn == 'Player':
|
362 |
-
with st.container():
|
363 |
-
st.markdown('<div class="box-container input-box">', unsafe_allow_html=True)
|
364 |
-
st.write("### ✏️ Your Turn!")
|
365 |
-
user_input = st.text_area("Write your next sentence:", key='story_input')
|
366 |
-
if st.button("✨ Submit", key="submit_button"):
|
367 |
-
if user_input:
|
368 |
-
process_player_input(user_input)
|
369 |
-
st.rerun()
|
370 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
371 |
-
else:
|
372 |
-
st.info("🤖 AI is thinking...") # แสดงสถานะเมื่อเป็น turn ของ AI
|
373 |
-
|
374 |
-
# Box 3: Help Buttons
|
375 |
-
with st.container():
|
376 |
-
st.markdown('<div class="box-container help-box">', unsafe_allow_html=True)
|
377 |
-
st.write("### 💡 Need Help?")
|
378 |
-
col1, col2, col3 = st.columns(3)
|
379 |
-
with col1:
|
380 |
-
if st.button("📚 Vocabulary Help"):
|
381 |
-
show_vocabulary_help()
|
382 |
-
with col2:
|
383 |
-
if st.button("❓ Writing Tips"):
|
384 |
-
show_writing_tips()
|
385 |
-
with col3:
|
386 |
-
if st.button("🎯 Story Ideas"):
|
387 |
-
show_story_ideas()
|
388 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
389 |
-
|
390 |
-
# Box 4: Rewards and Progress
|
391 |
-
with st.container():
|
392 |
-
st.markdown('<div class="box-container rewards-box">', unsafe_allow_html=True)
|
393 |
-
st.write("### 🏆 Your Achievements")
|
394 |
-
show_achievements()
|
395 |
-
col1, col2 = st.columns(2)
|
396 |
-
with col1:
|
397 |
-
if st.button("📥 Save Story"):
|
398 |
-
save_story()
|
399 |
-
with col2:
|
400 |
-
if st.button("🔄 Start New Story"):
|
401 |
-
reset_story()
|
402 |
-
st.rerun()
|
403 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
404 |
-
|
405 |
-
def process_player_input(user_input):
|
406 |
-
try:
|
407 |
-
# Get story context for AI
|
408 |
-
story_context = " ".join([entry['text'] for entry in st.session_state.story])
|
409 |
-
|
410 |
-
# Add player's sentence first
|
411 |
-
st.session_state.story.append({
|
412 |
-
'author': 'Player',
|
413 |
-
'text': user_input,
|
414 |
-
'timestamp': datetime.now().isoformat()
|
415 |
-
})
|
416 |
-
|
417 |
-
# Get AI's continuation immediately after player's input
|
418 |
-
ai_response = get_ai_story_continuation(
|
419 |
-
story_context + "\n" + user_input,
|
420 |
-
st.session_state.current_level
|
421 |
-
)
|
422 |
-
|
423 |
-
# Validate the continuation
|
424 |
-
is_coherent, reason = validate_story_quality(story_context, ai_response['continuation'])
|
425 |
-
|
426 |
-
if not is_coherent:
|
427 |
-
# Try getting a new response
|
428 |
-
ai_response = get_ai_story_continuation(story_context + "\n" + user_input, st.session_state.current_level)
|
429 |
-
|
430 |
-
# Add AI's response to story
|
431 |
-
st.session_state.story.append({
|
432 |
-
'author': 'AI',
|
433 |
-
'text': ai_response['continuation'],
|
434 |
-
'prompt': ai_response['creative_prompt'],
|
435 |
-
'vocabulary': ai_response['vocabulary_suggestions'],
|
436 |
-
'translation': ai_response.get('translation', ''),
|
437 |
-
'timestamp': datetime.now().isoformat()
|
438 |
-
})
|
439 |
-
|
440 |
-
# Get feedback for player's writing
|
441 |
-
feedback_response = openai.ChatCompletion.create(
|
442 |
-
model="gpt-4o-mini",
|
443 |
-
messages=[
|
444 |
-
{"role": "system", "content": f"""You are an English teacher helping a {st.session_state.current_level} level student.
|
445 |
-
Analyze their sentence and provide feedback in JSON format with:
|
446 |
-
1. grammar_check: List of grammar issues found (if any)
|
447 |
-
2. spelling_check: List of spelling issues found (if any)
|
448 |
-
3. improvement_suggestions: Specific suggestions for improvement
|
449 |
-
4. positive_feedback: What they did well
|
450 |
-
Be encouraging but thorough in your feedback."""},
|
451 |
-
{"role": "user", "content": f"Student's sentence: {user_input}"}
|
452 |
-
]
|
453 |
-
)
|
454 |
-
feedback = json.loads(feedback_response.choices[0].message.content)
|
455 |
-
|
456 |
-
# Update vocabulary used
|
457 |
-
words = set(user_input.lower().split())
|
458 |
-
st.session_state.vocabulary_used.update(words)
|
459 |
-
|
460 |
-
# Check for achievements
|
461 |
-
check_achievements(user_input, feedback)
|
462 |
-
|
463 |
-
# Show feedback to player
|
464 |
-
display_feedback(feedback)
|
465 |
-
|
466 |
-
except Exception as e:
|
467 |
-
st.error(f"Error processing input: {e}")
|
468 |
-
st.session_state.story.append({
|
469 |
-
'author': 'Player',
|
470 |
-
'text': user_input,
|
471 |
-
'timestamp': datetime.now().isoformat()
|
472 |
-
})
|
473 |
-
|
474 |
-
def check_achievements(user_input, feedback):
|
475 |
-
"""Check and award achievements based on player's writing"""
|
476 |
-
# Story length achievements
|
477 |
-
if len(st.session_state.story) >= 10:
|
478 |
-
award_badge("Storyteller")
|
479 |
-
if len(st.session_state.story) >= 20:
|
480 |
-
award_badge("Master Storyteller")
|
481 |
-
|
482 |
-
# Vocabulary achievements
|
483 |
-
if len(st.session_state.vocabulary_used) >= 20:
|
484 |
-
award_badge("Vocabulary Explorer")
|
485 |
-
if len(st.session_state.vocabulary_used) >= 50:
|
486 |
-
award_badge("Word Master")
|
487 |
-
|
488 |
-
# Writing quality achievements
|
489 |
-
if not feedback['grammar_check'] and not feedback['spelling_check']:
|
490 |
-
award_badge("Perfect Writing")
|
491 |
-
|
492 |
-
# Creativity achievements
|
493 |
-
if len(user_input.split()) >= 15:
|
494 |
-
award_badge("Detailed Writer")
|
495 |
-
|
496 |
-
def award_badge(badge_name):
|
497 |
-
"""Award a new badge if not already earned"""
|
498 |
-
if badge_name not in st.session_state.badges:
|
499 |
-
st.session_state.badges[badge_name] = 1
|
500 |
-
st.balloons()
|
501 |
-
st.success(f"🏆 New Achievement Unlocked: {badge_name}!")
|
502 |
-
else:
|
503 |
-
st.session_state.badges[badge_name] += 1
|
504 |
-
|
505 |
-
def display_feedback(feedback):
|
506 |
-
"""Display writing feedback to the player"""
|
507 |
-
with st.container():
|
508 |
-
# Show positive feedback first
|
509 |
-
st.success(f"👏 {feedback['positive_feedback']}")
|
510 |
-
|
511 |
-
# Show any grammar or spelling issues
|
512 |
-
if feedback['grammar_check']:
|
513 |
-
st.warning("📝 Grammar points to consider:")
|
514 |
-
for issue in feedback['grammar_check']:
|
515 |
-
st.write(f"- {issue}")
|
516 |
-
|
517 |
-
if feedback['spelling_check']:
|
518 |
-
st.warning("✍️ Spelling points to consider:")
|
519 |
-
for issue in feedback['spelling_check']:
|
520 |
-
st.write(f"- {issue}")
|
521 |
-
|
522 |
-
# Show improvement suggestions
|
523 |
-
if feedback['improvement_suggestions']:
|
524 |
-
st.info("💡 Suggestions for next time:")
|
525 |
-
for suggestion in feedback['improvement_suggestions']:
|
526 |
-
st.write(f"- {suggestion}")
|
527 |
-
|
528 |
-
def show_vocabulary_help():
|
529 |
-
"""Show vocabulary suggestions and meanings"""
|
530 |
-
if st.session_state.story:
|
531 |
-
last_entry = st.session_state.story[-1]
|
532 |
-
if 'vocabulary' in last_entry:
|
533 |
-
# Get word meanings and examples
|
534 |
-
try:
|
535 |
-
word_info = openai.ChatCompletion.create(
|
536 |
-
model="gpt-4o-mini",
|
537 |
-
messages=[
|
538 |
-
{"role": "system", "content": f"""For each word, provide:
|
539 |
-
1. Simple definition
|
540 |
-
2. Thai translation
|
541 |
-
3. Example sentence
|
542 |
-
Format in JSON with word as key."""},
|
543 |
-
{"role": "user", "content": f"Words: {', '.join(last_entry['vocabulary'])}"}
|
544 |
-
]
|
545 |
-
)
|
546 |
-
word_details = json.loads(word_info.choices[0].message.content)
|
547 |
-
|
548 |
-
st.write("### 📚 Suggested Words:")
|
549 |
-
for word in last_entry['vocabulary']:
|
550 |
-
with st.expander(f"🔤 {word}"):
|
551 |
-
if word in word_details:
|
552 |
-
details = word_details[word]
|
553 |
-
st.write(f"**Meaning:** {details['definition']}")
|
554 |
-
st.write(f"**แปลไทย:** {details['thai']}")
|
555 |
-
st.write(f"**Example:** _{details['example']}_")
|
556 |
-
except Exception as e:
|
557 |
-
# Fallback to simple display
|
558 |
-
st.write("Try using these words:")
|
559 |
-
for word in last_entry['vocabulary']:
|
560 |
-
st.markdown(f"- *{word}*")
|
561 |
-
|
562 |
-
def show_writing_tips():
|
563 |
-
"""Show writing tips based on current level and story context"""
|
564 |
-
if st.session_state.story:
|
565 |
-
try:
|
566 |
-
# Get personalized writing tips
|
567 |
-
tips_response = openai.ChatCompletion.create(
|
568 |
-
model="gpt-4o-mini",
|
569 |
-
messages=[
|
570 |
-
{"role": "system", "content": f"""You are helping a {st.session_state.current_level} level English student.
|
571 |
-
Provide 3 specific writing tips based on their level and story context.
|
572 |
-
Include examples for each tip.
|
573 |
-
Keep tips short and friendly."""},
|
574 |
-
{"role": "user", "content": f"Story so far: {' '.join([entry['text'] for entry in st.session_state.story])}"}
|
575 |
-
]
|
576 |
-
)
|
577 |
-
|
578 |
-
st.write("### ✍️ Writing Tips")
|
579 |
-
tips = tips_response.choices[0].message.content.split('\n')
|
580 |
-
for tip in tips:
|
581 |
-
if tip.strip():
|
582 |
-
st.info(tip)
|
583 |
-
|
584 |
-
# Add quick reference examples
|
585 |
-
st.write("### 🎯 Quick Examples:")
|
586 |
-
col1, col2 = st.columns(2)
|
587 |
-
with col1:
|
588 |
-
st.write("**Good:**")
|
589 |
-
st.success("The happy dog played in the garden.")
|
590 |
-
with col2:
|
591 |
-
st.write("**Better:**")
|
592 |
-
st.success("The excited golden retriever chased butterflies in the colorful garden.")
|
593 |
-
|
594 |
-
except Exception as e:
|
595 |
-
# Fallback tips
|
596 |
-
st.write("### General Writing Tips:")
|
597 |
-
st.write("1. Use descriptive words")
|
598 |
-
st.write("2. Keep your sentences clear")
|
599 |
-
st.write("3. Try to be creative")
|
600 |
-
|
601 |
-
def show_story_ideas():
|
602 |
-
"""Generate creative story ideas based on current context"""
|
603 |
-
if st.session_state.story:
|
604 |
-
try:
|
605 |
-
# Get creative prompts
|
606 |
-
ideas_response = openai.ChatCompletion.create(
|
607 |
-
model="gpt-4o-mini",
|
608 |
-
messages=[
|
609 |
-
{"role": "system", "content": f"""Generate 3 creative story direction ideas for a {st.session_state.current_level} student.
|
610 |
-
Ideas should follow from the current story.
|
611 |
-
Make suggestions exciting but achievable for their level."""},
|
612 |
-
{"role": "user", "content": f"Story so far: {' '.join([entry['text'] for entry in st.session_state.story])}"}
|
613 |
-
]
|
614 |
-
)
|
615 |
-
|
616 |
-
st.write("### 💭 Story Ideas")
|
617 |
-
ideas = ideas_response.choices[0].message.content.split('\n')
|
618 |
-
for i, idea in enumerate(ideas, 1):
|
619 |
-
if idea.strip():
|
620 |
-
with st.expander(f"Idea {i}"):
|
621 |
-
st.write(idea)
|
622 |
-
|
623 |
-
# Add story element suggestions
|
624 |
-
st.write("### 🎨 Try adding:")
|
625 |
-
elements = ["a new character", "a surprise event", "a funny moment", "a problem to solve"]
|
626 |
-
cols = st.columns(len(elements))
|
627 |
-
for col, element in zip(cols, elements):
|
628 |
-
with col:
|
629 |
-
st.button(element, key=f"element_{element}")
|
630 |
-
|
631 |
-
except Exception as e:
|
632 |
-
# Fallback ideas
|
633 |
-
st.write("### Story Ideas:")
|
634 |
-
st.write("1. Introduce a new character")
|
635 |
-
st.write("2. Add an unexpected event")
|
636 |
-
st.write("3. Create a problem to solve")
|
637 |
-
|
638 |
-
def show_achievements():
|
639 |
-
"""Display achievements and badges with animations"""
|
640 |
-
if st.session_state.badges:
|
641 |
-
st.write("### 🏆 Your Achievements")
|
642 |
-
cols = st.columns(3)
|
643 |
-
for i, (badge, count) in enumerate(st.session_state.badges.items()):
|
644 |
-
with cols[i % 3]:
|
645 |
-
st.markdown(f'''
|
646 |
-
<div class="badge-container" style="animation: bounce 1s infinite;">
|
647 |
-
<div style="font-size: 2em;">🌟</div>
|
648 |
-
<div style="font-weight: bold;">{badge}</div>
|
649 |
-
<div>x{count}</div>
|
650 |
-
</div>
|
651 |
-
''', unsafe_allow_html=True)
|
652 |
-
|
653 |
-
# Add progress towards next badge
|
654 |
-
st.write("### 📈 Progress to Next Badge:")
|
655 |
-
current_words = len(st.session_state.vocabulary_used)
|
656 |
-
next_milestone = (current_words // 10 + 1) * 10
|
657 |
-
progress = current_words / next_milestone
|
658 |
-
st.progress(progress)
|
659 |
-
st.write(f"Use {next_milestone - current_words} more unique words for next badge!")
|
660 |
-
|
661 |
-
def save_story():
|
662 |
-
"""Save story with additional information"""
|
663 |
-
if st.session_state.story:
|
664 |
-
# Prepare story text with formatting
|
665 |
-
story_parts = []
|
666 |
-
story_parts.append("=== My English Story Adventure ===\n")
|
667 |
-
story_parts.append(f"Level: {st.session_state.current_level}")
|
668 |
-
story_parts.append(f"Date: {datetime.now().strftime('%Y-%m-%d')}\n")
|
669 |
-
|
670 |
-
# Add story content
|
671 |
-
for entry in st.session_state.story:
|
672 |
-
author = "👤 You:" if entry['author'] == 'Player' else "🤖 AI:"
|
673 |
-
story_parts.append(f"{author} {entry['text']}")
|
674 |
-
|
675 |
-
# Add achievements
|
676 |
-
story_parts.append("\n=== Achievements ===")
|
677 |
-
for badge, count in st.session_state.badges.items():
|
678 |
-
story_parts.append(f"🌟 {badge}: x{count}")
|
679 |
-
|
680 |
-
# Create the full story text
|
681 |
-
story_text = "\n".join(story_parts)
|
682 |
-
|
683 |
-
# Offer download options
|
684 |
-
col1, col2 = st.columns(2)
|
685 |
-
with col1:
|
686 |
-
st.download_button(
|
687 |
-
label="📝 Download Story (Text)",
|
688 |
-
data=story_text,
|
689 |
-
file_name="my_story.txt",
|
690 |
-
mime="text/plain"
|
691 |
-
)
|
692 |
-
with col2:
|
693 |
-
# Create JSON version with more details
|
694 |
-
story_data = {
|
695 |
-
"metadata": {
|
696 |
-
"level": st.session_state.current_level,
|
697 |
-
"date": datetime.now().isoformat(),
|
698 |
-
"vocabulary_used": list(st.session_state.vocabulary_used),
|
699 |
-
"achievements": dict(st.session_state.badges)
|
700 |
-
},
|
701 |
-
"story": st.session_state.story
|
702 |
-
}
|
703 |
-
st.download_button(
|
704 |
-
label="💾 Download Progress Report (JSON)",
|
705 |
-
data=json.dumps(story_data, indent=2),
|
706 |
-
file_name="story_progress.json",
|
707 |
-
mime="application/json"
|
708 |
-
)
|
709 |
-
|
710 |
-
def reset_story():
|
711 |
-
"""Reset all story-related session state"""
|
712 |
-
if st.button("🔄 Start New Story", key="reset_button"):
|
713 |
-
st.session_state.story = []
|
714 |
-
st.session_state.story_started = False
|
715 |
-
st.session_state.first_turn = None
|
716 |
-
st.session_state.current_turn = None
|
717 |
-
st.session_state.vocabulary_used = set()
|
718 |
-
# Keep badges for long-term progress
|
719 |
-
st.balloons()
|
720 |
-
st.success("Ready for a new adventure! Your achievements have been saved.")
|
721 |
-
st.rerun()
|
722 |
-
|
723 |
-
|
724 |
-
if __name__ == "__main__":
|
725 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app_work.py
ADDED
@@ -0,0 +1,2212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# === 1. IMPORTS & SETUP ===
|
2 |
+
import streamlit as st
|
3 |
+
import json
|
4 |
+
import datetime
|
5 |
+
import logging
|
6 |
+
from openai import OpenAI
|
7 |
+
from typing import Dict, List, Set, Tuple
|
8 |
+
import io
|
9 |
+
from reportlab.lib import colors
|
10 |
+
from reportlab.lib.pagesizes import A4
|
11 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
12 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
13 |
+
from datetime import datetime
|
14 |
+
import random
|
15 |
+
|
16 |
+
# === 2. CONFIGURATIONS ===
|
17 |
+
# Theme Configuration
|
18 |
+
story_themes = {
|
19 |
+
'fantasy': {
|
20 |
+
'id': 'fantasy',
|
21 |
+
'icon': '🏰',
|
22 |
+
'name_en': 'Fantasy & Magic',
|
23 |
+
'name_th': 'แฟนตาซีและเวทมนตร์',
|
24 |
+
'description_th': 'ผจญภัยในโลกแห่งเวทมนตร์และจินตนาการ',
|
25 |
+
'description_en': 'Adventure in a world of magic and imagination',
|
26 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
27 |
+
'vocabulary': {
|
28 |
+
'Beginner': ['dragon', 'magic', 'wand', 'spell', 'wizard', 'fairy', 'castle', 'king', 'queen'],
|
29 |
+
'Intermediate': ['potion', 'enchanted', 'castle', 'creature', 'power', 'scroll', 'portal', 'magical'],
|
30 |
+
'Advanced': ['sorcery', 'mystical', 'enchantment', 'prophecy', 'ancient', 'legendary', 'mythical']
|
31 |
+
},
|
32 |
+
'story_starters': {
|
33 |
+
'Beginner': [
|
34 |
+
{'th': 'วันหนึ่งฉันเจอไม้วิเศษในสวน...', 'en': 'One day, I found a magic wand in the garden...'},
|
35 |
+
{'th': 'มังกรน้อยกำลังมองหาเพื่อน...', 'en': 'The little dragon was looking for a friend...'},
|
36 |
+
{'th': 'เจ้าหญิงน้อยมีความลับวิเศษ...', 'en': 'The little princess had a magical secret...'}
|
37 |
+
],
|
38 |
+
'Intermediate': [
|
39 |
+
{'th': 'ในปราสาทเก่าแก่มีประตูลึกลับ...', 'en': 'In the ancient castle, there was a mysterious door...'},
|
40 |
+
{'th': 'เมื่อน้ำยาวิเศษเริ่มส่องแสง...', 'en': 'When the magic potion started to glow...'},
|
41 |
+
{'th': 'หนังสือเวทมนตร์เล่มนั้นเปิดออกเอง...', 'en': 'The spellbook opened by itself...'}
|
42 |
+
],
|
43 |
+
'Advanced': [
|
44 |
+
{'th': 'คำทำนายโบราณกล่าวถึงผู้วิเศษที่จะมา...', 'en': 'The ancient prophecy spoke of a wizard who would come...'},
|
45 |
+
{'th': 'ในโลกที่เวทมนตร์กำลังจะสูญหาย...', 'en': 'In a world where magic was fading away...'},
|
46 |
+
{'th': 'ณ จุดบรรจบของดวงดาวทั้งห้า...', 'en': 'At the convergence of the five stars...'}
|
47 |
+
]
|
48 |
+
},
|
49 |
+
'background_color': '#E8F3FF',
|
50 |
+
'accent_color': '#1E88E5'
|
51 |
+
},
|
52 |
+
'nature': {
|
53 |
+
'id': 'nature',
|
54 |
+
'icon': '🌳',
|
55 |
+
'name_en': 'Nature & Animals',
|
56 |
+
'name_th': 'ธรรมชาติและสัตว์โลก',
|
57 |
+
'description_th': 'เรื่องราวของสัตว์น้อยใหญ่และธรรมชาติอันงดงาม',
|
58 |
+
'description_en': 'Stories of animals and beautiful nature',
|
59 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
60 |
+
'vocabulary': {
|
61 |
+
'Beginner': ['tree', 'bird', 'flower', 'cat', 'dog', 'garden', 'rabbit', 'butterfly', 'sun'],
|
62 |
+
'Intermediate': ['forest', 'river', 'mountain', 'wildlife', 'season', 'weather', 'rainbow', 'stream'],
|
63 |
+
'Advanced': ['ecosystem', 'habitat', 'wilderness', 'environment', 'conservation', 'migration', 'climate']
|
64 |
+
},
|
65 |
+
'story_starters': {
|
66 |
+
'Beginner': [
|
67 |
+
{'th': 'แมวน้อยเจอนกในสวน...', 'en': 'The little cat found a bird in the garden...'},
|
68 |
+
{'th': 'ดอกไม้สวยกำลังเบ่งบาน...', 'en': 'The beautiful flower was blooming...'},
|
69 |
+
{'th': 'กระต่ายน้อยหลงทางในสวน...', 'en': 'The little rabbit got lost in the garden...'}
|
70 |
+
],
|
71 |
+
'Intermediate': [
|
72 |
+
{'th': 'ในป่าใหญ่มีเสียงลึกลับ...', 'en': 'In the big forest, there was a mysterious sound...'},
|
73 |
+
{'th': 'แม่น้ำสายนี้มีความลับ...', 'en': 'This river had a secret...'},
|
74 |
+
{'th': 'สายรุ้งพาดผ่านภูเขา...', 'en': 'A rainbow stretched across the mountain...'}
|
75 |
+
],
|
76 |
+
'Advanced': [
|
77 |
+
{'th': 'ฝูงนกกำลังอพยพย้ายถิ่น...', 'en': 'The birds were migrating...'},
|
78 |
+
{'th': 'ป่าฝนกำลังเปลี่ยนแปลง...', 'en': 'The rainforest was changing...'},
|
79 |
+
{'th': 'ความลับของระบบนิเวศ...', 'en': 'The secret of the ecosystem...'}
|
80 |
+
]
|
81 |
+
},
|
82 |
+
'background_color': '#F1F8E9',
|
83 |
+
'accent_color': '#4CAF50'
|
84 |
+
},
|
85 |
+
'space': {
|
86 |
+
'id': 'space',
|
87 |
+
'icon': '🚀',
|
88 |
+
'name_en': 'Space Adventure',
|
89 |
+
'name_th': 'ผจญภัยในอวกาศ',
|
90 |
+
'description_th': 'เรื่องราวการสำรวจอวกาศและดวงดาวอันน่าตื่นเต้น',
|
91 |
+
'description_en': 'Exciting stories of space exploration and celestial discoveries',
|
92 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
93 |
+
'vocabulary': {
|
94 |
+
'Beginner': ['star', 'moon', 'planet', 'sun', 'rocket', 'alien', 'space', 'light'],
|
95 |
+
'Intermediate': ['astronaut', 'spacecraft', 'galaxy', 'meteor', 'satellite', 'orbit', 'comet'],
|
96 |
+
'Advanced': ['constellation', 'nebula', 'astronomy', 'telescope', 'exploration', 'discovery']
|
97 |
+
},
|
98 |
+
'story_starters': {
|
99 |
+
'Beginner': [
|
100 |
+
{'th': 'จรวดลำน้อยพร้อมบินแล้ว...', 'en': 'The little rocket was ready to fly...'},
|
101 |
+
{'th': 'ดาวดวงน้อยเปล่งแสงวิบวับ...', 'en': 'The little star twinkled brightly...'},
|
102 |
+
{'th': 'มนุษย์ต่างดาวที่เป็นมิตร...', 'en': 'The friendly alien...'}
|
103 |
+
],
|
104 |
+
'Intermediate': [
|
105 |
+
{'th': 'นักบินอวกาศพบสิ่งประหลาด...', 'en': 'The astronaut found something strange...'},
|
106 |
+
{'th': 'ดาวเคราะห์ดวงใหม่ถูกค้นพบ...', 'en': 'A new planet was discovered...'},
|
107 |
+
{'th': 'สถานีอวกาศส่งสัญญาณลึกลับ...', 'en': 'The space station sent a mysterious signal...'}
|
108 |
+
],
|
109 |
+
'Advanced': [
|
110 |
+
{'th': 'การสำรวจดาวหางนำไปสู่การค้นพบ...', 'en': 'The comet exploration led to a discovery...'},
|
111 |
+
{'th': 'กาแล็กซี่ที่ไม่มีใครเคยเห็น...', 'en': 'An unknown galaxy appeared...'},
|
112 |
+
{'th': 'ความลับของหลุมดำ...', 'en': 'The secret of the black hole...'}
|
113 |
+
]
|
114 |
+
},
|
115 |
+
'background_color': '#E1F5FE',
|
116 |
+
'accent_color': '#0288D1'
|
117 |
+
},
|
118 |
+
'adventure': {
|
119 |
+
'id': 'adventure',
|
120 |
+
'icon': '🗺️',
|
121 |
+
'name_en': 'Adventure & Quest',
|
122 |
+
'name_th': 'การผจญภัยและการค้นหา',
|
123 |
+
'description_th': 'ออกผจญภัยค้นหาสมบัติและความลับต่างๆ',
|
124 |
+
'description_en': 'Embark on quests to find treasures and secrets',
|
125 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
126 |
+
'vocabulary': {
|
127 |
+
'Beginner': ['map', 'treasure', 'cave', 'island', 'path', 'boat', 'key', 'chest'],
|
128 |
+
'Intermediate': ['compass', 'adventure', 'journey', 'mystery', 'explore', 'discover', 'quest'],
|
129 |
+
'Advanced': ['expedition', 'archaeology', 'artifact', 'ancient', 'mysterious', 'discovery']
|
130 |
+
},
|
131 |
+
'story_starters': {
|
132 |
+
'Beginner': [
|
133 |
+
{'th': 'แผนที่เก่าแก่ชิ้นหนึ่ง...', 'en': 'An old map showed...'},
|
134 |
+
{'th': 'บนเกาะเล็กๆ มีสมบัติ...', 'en': 'On a small island, there was a treasure...'},
|
135 |
+
{'th': 'ถ้ำลึกลับถูกค้นพบ...', 'en': 'A mysterious cave was found...'}
|
136 |
+
],
|
137 |
+
'Intermediate': [
|
138 |
+
{'th': 'เข็มทิศวิเศษชี้ไปที่...', 'en': 'The magical compass pointed to...'},
|
139 |
+
{'th': 'การเดินทางเริ่มต้นที่...', 'en': 'The journey began at...'},
|
140 |
+
{'th': 'ความลับของวัตถุโบราณ...', 'en': 'The secret of the ancient artifact...'}
|
141 |
+
],
|
142 |
+
'Advanced': [
|
143 |
+
{'th': 'การสำรวจซากปรักหักพังนำไปสู่...', 'en': 'The ruins exploration led to...'},
|
144 |
+
{'th': 'นักโบราณคดีค้นพบ...', 'en': 'The archaeologist discovered...'},
|
145 |
+
{'th': 'ความลับของอารยธรรมโบราณ...', 'en': 'The secret of the ancient civilization...'}
|
146 |
+
]
|
147 |
+
},
|
148 |
+
'background_color': '#FFF3E0',
|
149 |
+
'accent_color': '#FF9800'
|
150 |
+
},
|
151 |
+
'school': {
|
152 |
+
'id': 'school',
|
153 |
+
'icon': '🏫',
|
154 |
+
'name_en': 'School & Friends',
|
155 |
+
'name_th': 'โรงเรียนและเพื่อน',
|
156 |
+
'description_th': 'เรื่องราวสนุกๆ ในโรงเรียนกับเพื่อนๆ',
|
157 |
+
'description_en': 'Fun stories about school life and friendship',
|
158 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
159 |
+
'vocabulary': {
|
160 |
+
'Beginner': ['friend', 'teacher', 'book', 'classroom', 'pencil', 'desk', 'lunch', 'play'],
|
161 |
+
'Intermediate': ['homework', 'project', 'library', 'playground', 'student', 'lesson', 'study'],
|
162 |
+
'Advanced': ['presentation', 'experiment', 'knowledge', 'research', 'collaboration', 'achievement']
|
163 |
+
},
|
164 |
+
'story_starters': {
|
165 |
+
'Beginner': [
|
166 |
+
{'th': 'วันแรกในห้องเรียนใหม่...', 'en': 'First day in the new classroom...'},
|
167 |
+
{'th': 'เพื่อนใหม่ในโรงเรียน...', 'en': 'A new friend at school...'},
|
168 |
+
{'th': 'ที่โต๊ะอาหารกลางวัน...', 'en': 'At the lunch table...'}
|
169 |
+
],
|
170 |
+
'Intermediate': [
|
171 |
+
{'th': 'โครงงานพิเศษของห้องเรา...', 'en': 'Our class special project...'},
|
172 |
+
{'th': 'ในห้องสมุดมีความลับ...', 'en': 'The library had a secret...'},
|
173 |
+
{'th': 'การทดลองวิทยาศาสตร์ครั้งนี้...', 'en': 'This science experiment...'}
|
174 |
+
],
|
175 |
+
'Advanced': [
|
176 |
+
{'th': 'การนำเสนอครั้งสำคัญ...', 'en': 'The important presentation...'},
|
177 |
+
{'th': 'การค้นคว้าพิเศษนำไปสู่...', 'en': 'The special research led to...'},
|
178 |
+
{'th': 'โครงการความร่วมมือระหว่างห้อง...', 'en': 'The inter-class collaboration project...'}
|
179 |
+
]
|
180 |
+
},
|
181 |
+
'background_color': '#F3E5F5',
|
182 |
+
'accent_color': '#9C27B0'
|
183 |
+
},
|
184 |
+
'superhero': {
|
185 |
+
'id': 'superhero',
|
186 |
+
'icon': '🦸',
|
187 |
+
'name_en': 'Superheroes',
|
188 |
+
'name_th': 'ซูเปอร์ฮีโร่',
|
189 |
+
'description_th': 'เรื่องราวของฮีโร่ตัวน้อยผู้ช่วยเหลือผู้อื่น',
|
190 |
+
'description_en': 'Stories of young heroes helping others',
|
191 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
192 |
+
'vocabulary': {
|
193 |
+
'Beginner': ['hero', 'help', 'save', 'power', 'mask', 'cape', 'fly', 'strong'],
|
194 |
+
'Intermediate': ['rescue', 'protect', 'brave', 'courage', 'mission', 'team', 'secret', 'mighty'],
|
195 |
+
'Advanced': ['superhero', 'extraordinary', 'responsibility', 'leadership', 'determination', 'justice']
|
196 |
+
},
|
197 |
+
'story_starters': {
|
198 |
+
'Beginner': [
|
199 |
+
{'th': 'ฮีโร่น้อยคนใหม่ของเมือง...', 'en': 'The city\'s new young hero...'},
|
200 |
+
{'th': 'พลังพิเศษของฉันทำให้...', 'en': 'My special power made me...'},
|
201 |
+
{'th': 'เมื่อต้องช่วยเหลือแมวตัวน้อย...', 'en': 'When I had to save a little cat...'}
|
202 |
+
],
|
203 |
+
'Intermediate': [
|
204 |
+
{'th': 'ภารกิจลับของทีมฮีโร่...', 'en': 'The hero team\'s secret mission...'},
|
205 |
+
{'th': 'พลังใหม่ที่น่าประหลาดใจ...', 'en': 'A surprising new power...'},
|
206 |
+
{'th': 'การช่วยเหลือครั้งสำคัญ...', 'en': 'An important rescue mission...'}
|
207 |
+
],
|
208 |
+
'Advanced': [
|
209 |
+
{'th': 'ความรับผิดชอบของการเป็นฮีโร่...', 'en': 'The responsibility of being a hero...'},
|
210 |
+
{'th': 'เมื่อเมืองต้องการฮีโร่...', 'en': 'When the city needed a hero...'},
|
211 |
+
{'th': 'การต่อสู้เพื่อความยุติธรรม...', 'en': 'Fighting for justice...'}
|
212 |
+
]
|
213 |
+
},
|
214 |
+
'background_color': '#FFE0B2',
|
215 |
+
'accent_color': '#F57C00'
|
216 |
+
},
|
217 |
+
'mystery': {
|
218 |
+
'id': 'mystery',
|
219 |
+
'icon': '🔍',
|
220 |
+
'name_en': 'Mystery & Detective',
|
221 |
+
'name_th': 'ไขปริศนาและนักสืบ',
|
222 |
+
'description_th': 'สืบสวนปริศนาและไขความลับต่างๆ',
|
223 |
+
'description_en': 'Solve mysteries and uncover secrets',
|
224 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
225 |
+
'vocabulary': {
|
226 |
+
'Beginner': ['clue', 'find', 'look', 'search', 'mystery', 'hidden', 'secret', 'detective'],
|
227 |
+
'Intermediate': ['investigate', 'evidence', 'puzzle', 'solve', 'discover', 'suspicious', 'case'],
|
228 |
+
'Advanced': ['investigation', 'deduction', 'enigma', 'cryptic', 'mysterious', 'revelation']
|
229 |
+
},
|
230 |
+
'story_starters': {
|
231 |
+
'Beginner': [
|
232 |
+
{'th': 'มีรอยปริศนาในสวน...', 'en': 'There were mysterious footprints in the garden...'},
|
233 |
+
{'th': 'จดหมายลึกลับถูกส่งมา...', 'en': 'A mysterious letter arrived...'},
|
234 |
+
{'th': 'ของเล่นหายไปอย่างลึกลับ...', 'en': 'The toy mysteriously disappeared...'}
|
235 |
+
],
|
236 |
+
'Intermediate': [
|
237 |
+
{'th': 'เบาะแสชิ้นแรกนำไปสู่...', 'en': 'The first clue led to...'},
|
238 |
+
{'th': 'ความลับในห้องเก่า...', 'en': 'The secret in the old room...'},
|
239 |
+
{'th': 'รหัสลับถูกค้นพบ...', 'en': 'A secret code was found...'}
|
240 |
+
],
|
241 |
+
'Advanced': [
|
242 |
+
{'th': 'คดีปริศนาที่ยากที่สุด...', 'en': 'The most challenging mystery case...'},
|
243 |
+
{'th': 'ความลับที่ซ่อนอยู่มานาน...', 'en': 'A long-hidden secret...'},
|
244 |
+
{'th': 'การสืบสวนนำไปสู่การค้นพบ...', 'en': 'The investigation led to a discovery...'}
|
245 |
+
]
|
246 |
+
},
|
247 |
+
'background_color': '#E0E0E0',
|
248 |
+
'accent_color': '#616161'
|
249 |
+
},
|
250 |
+
'science': {
|
251 |
+
'id': 'science',
|
252 |
+
'icon': '🔬',
|
253 |
+
'name_en': 'Science & Discovery',
|
254 |
+
'name_th': 'วิทยาศาสตร์และการค้นพบ',
|
255 |
+
'description_th': 'การทดลองและค้นพบทางวิทยาศาสตร์ที่น่าตื่นเต้น',
|
256 |
+
'description_en': 'Exciting scientific experiments and discoveries',
|
257 |
+
'level_range': ['Beginner', 'Intermediate', 'Advanced'],
|
258 |
+
'vocabulary': {
|
259 |
+
'Beginner': ['experiment', 'science', 'lab', 'test', 'mix', 'observe', 'change', 'result'],
|
260 |
+
'Intermediate': ['hypothesis', 'research', 'discovery', 'invention', 'laboratory', 'scientist'],
|
261 |
+
'Advanced': ['innovation', 'technological', 'breakthrough', 'analysis', 'investigation']
|
262 |
+
},
|
263 |
+
'story_starters': {
|
264 |
+
'Beginner': [
|
265 |
+
{'th': 'การทดลองง่ายๆ เริ่มต้นด้วย...', 'en': 'The simple experiment started with...'},
|
266 |
+
{'th': 'ในห้องทดลองมีสิ่งมหัศจรรย์...', 'en': 'In the lab, there was something amazing...'},
|
267 |
+
{'th': 'เมื่อผสมสองสิ่งเข้าด้วยกัน...', 'en': 'When mixing the two things together...'}
|
268 |
+
],
|
269 |
+
'Intermediate': [
|
270 |
+
{'th': 'การค้นพบที่น่าประหลาดใจ...', 'en': 'A surprising discovery...'},
|
271 |
+
{'th': 'สิ่งประดิษฐ์ใหม่ทำให้...', 'en': 'The new invention made...'},
|
272 |
+
{'th': 'การทดลองที่ไม่คาดคิด...', 'en': 'An unexpected experiment...'}
|
273 |
+
],
|
274 |
+
'Advanced': [
|
275 |
+
{'th': 'นวัตกรรมที่จะเปลี่ยนโลก...', 'en': 'Innovation that would change the world...'},
|
276 |
+
{'th': 'การค้นพบทางวิทยาศาสตร์ครั้งสำคัญ...', 'en': 'An important scientific discovery...'},
|
277 |
+
{'th': 'เทคโนโลยีใหม่ที่น่าทึ่ง...', 'en': 'Amazing new technology...'}
|
278 |
+
]
|
279 |
+
},
|
280 |
+
'background_color': '#E8EAF6',
|
281 |
+
'accent_color': '#3F51B5'
|
282 |
+
}
|
283 |
+
}
|
284 |
+
|
285 |
+
# Set up logging
|
286 |
+
logging.basicConfig(
|
287 |
+
level=logging.INFO,
|
288 |
+
format='%(asctime)s - %(levelname)s - %(message)s'
|
289 |
+
)
|
290 |
+
|
291 |
+
# === 2. CONFIGURATIONS ===
|
292 |
+
# Page Config
|
293 |
+
st.set_page_config(
|
294 |
+
page_title="JoyStory - Interactive Story Adventure",
|
295 |
+
page_icon="📖",
|
296 |
+
layout="wide",
|
297 |
+
initial_sidebar_state="collapsed"
|
298 |
+
)
|
299 |
+
|
300 |
+
# Initialize OpenAI client
|
301 |
+
try:
|
302 |
+
client = OpenAI()
|
303 |
+
except Exception as e:
|
304 |
+
logging.error(f"Failed to initialize OpenAI client: {str(e)}")
|
305 |
+
st.error("Failed to initialize AI services. Please try again later.")
|
306 |
+
|
307 |
+
# Define Constants
|
308 |
+
MAX_RETRIES = 3
|
309 |
+
DEFAULT_LEVEL = 'Beginner'
|
310 |
+
SUPPORTED_LANGUAGES = ['th', 'en']
|
311 |
+
|
312 |
+
# Achievement Requirements
|
313 |
+
ACHIEVEMENT_THRESHOLDS = {
|
314 |
+
'perfect_writer': 5, # 5 correct sentences in a row
|
315 |
+
'vocabulary_master': 50, # 50 unique words used
|
316 |
+
'story_master': 10, # 10 sentences in story
|
317 |
+
'accuracy_king': 80 # 80% accuracy rate
|
318 |
+
}
|
319 |
+
|
320 |
+
# Level Configuration
|
321 |
+
level_options = {
|
322 |
+
'Beginner': {
|
323 |
+
'thai_name': 'ระดับเริ่มต้น (ป.1-3)',
|
324 |
+
'age_range': '7-9 ปี',
|
325 |
+
'description': 'เหมาะสำหรับน้องๆ ที่เริ่มเรียนรู้การเขียนประโยคภาษาอังกฤษ',
|
326 |
+
'features': [
|
327 |
+
'ประโยคสั้นๆ ง่ายๆ',
|
328 |
+
'คำศัพท์พื้นฐานที่ใช้ในชีวิตประจำวัน',
|
329 |
+
'มีคำแนะนำภาษาไทยละเอียด',
|
330 |
+
'เน้นการใช้ Present Simple Tense'
|
331 |
+
],
|
332 |
+
'max_sentence_length': 10,
|
333 |
+
'allowed_tenses': ['present_simple'],
|
334 |
+
'feedback_level': 'detailed'
|
335 |
+
},
|
336 |
+
'Intermediate': {
|
337 |
+
'thai_name': 'ระดับกลาง (ป.4-6)',
|
338 |
+
'age_range': '10-12 ปี',
|
339 |
+
'description': 'เหมาะสำหรับน้องๆ ที่สามารถเขียนประโยคพื้นฐานได้แล้ว',
|
340 |
+
'features': [
|
341 |
+
'ประโยคซับซ้อนขึ้น',
|
342 |
+
'เริ่มใช้ Past Tense ได้',
|
343 |
+
'คำศัพท์หลากหลายขึ้น',
|
344 |
+
'สามารถเขียนเรื่องราวต่อเนื่องได้'
|
345 |
+
],
|
346 |
+
'max_sentence_length': 15,
|
347 |
+
'allowed_tenses': ['present_simple', 'past_simple'],
|
348 |
+
'feedback_level': 'moderate'
|
349 |
+
},
|
350 |
+
'Advanced': {
|
351 |
+
'thai_name': 'ระดับก้าวหน้า (ม.1-3)',
|
352 |
+
'age_range': '13-15 ปี',
|
353 |
+
'description': 'เหมาะสำหรับน้องๆ ที่มีพื้นฐานภาษาอังกฤษดี',
|
354 |
+
'features': [
|
355 |
+
'เขียนเรื่องราวได้หลากหลายรูปแบบ',
|
356 |
+
'ใช้ Tense ต่างๆ ได้',
|
357 |
+
'คำศัพท์ระดับสูงขึ้น',
|
358 |
+
'สามารถแต่งเรื่องที่ซับซ้อนได้'
|
359 |
+
],
|
360 |
+
'max_sentence_length': 20,
|
361 |
+
'allowed_tenses': ['present_simple', 'past_simple', 'present_perfect', 'past_perfect'],
|
362 |
+
'feedback_level': 'concise'
|
363 |
+
}
|
364 |
+
}
|
365 |
+
|
366 |
+
# Achievement Configuration
|
367 |
+
achievements_list = {
|
368 |
+
'perfect_writer': {
|
369 |
+
'name': '🌟 นักเขียนไร้ที่ติ',
|
370 |
+
'description': 'เขียนถูกต้อง 5 ประโยคติดต่อกัน',
|
371 |
+
'condition': lambda: st.session_state.points['streak'] >= ACHIEVEMENT_THRESHOLDS['perfect_writer']
|
372 |
+
},
|
373 |
+
'vocabulary_master': {
|
374 |
+
'name': '📚 ราชาคำศัพท์',
|
375 |
+
'description': 'ใช้คำศัพท์ไม่ซ้ำกัน 50 คำ',
|
376 |
+
'condition': lambda: len(st.session_state.stats['vocabulary_used']) >= ACHIEVEMENT_THRESHOLDS['vocabulary_master']
|
377 |
+
},
|
378 |
+
'story_master': {
|
379 |
+
'name': '📖 นักแต่งนิทาน',
|
380 |
+
'description': 'เขียนเรื่องยาว 10 ประโยค',
|
381 |
+
'condition': lambda: len(st.session_state.story) >= ACHIEVEMENT_THRESHOLDS['story_master']
|
382 |
+
},
|
383 |
+
'accuracy_king': {
|
384 |
+
'name': '👑 ราชาความแม่นยำ',
|
385 |
+
'description': 'มีอัตราความถูกต้อง 80% ขึ้นไป (อย่างน้อย 10 ประโยค)',
|
386 |
+
'condition': lambda: (
|
387 |
+
st.session_state.stats['total_sentences'] >= 10 and
|
388 |
+
st.session_state.stats['accuracy_rate'] >= ACHIEVEMENT_THRESHOLDS['accuracy_king']
|
389 |
+
)
|
390 |
+
}
|
391 |
+
}
|
392 |
+
|
393 |
+
# Initial CSS Setup
|
394 |
+
st.markdown("""
|
395 |
+
<style>
|
396 |
+
/* Base Styles */
|
397 |
+
@import url('https://fonts.googleapis.com/css2?family=Sarabun:wght@400;600&display=swap');
|
398 |
+
|
399 |
+
body {
|
400 |
+
font-family: 'Sarabun', sans-serif;
|
401 |
+
}
|
402 |
+
|
403 |
+
/* Custom Classes */
|
404 |
+
.thai-eng {
|
405 |
+
font-size: 1.1em;
|
406 |
+
padding: 10px;
|
407 |
+
background-color: #f8f9fa;
|
408 |
+
border-radius: 8px;
|
409 |
+
margin: 10px 0;
|
410 |
+
}
|
411 |
+
|
412 |
+
.theme-card {
|
413 |
+
transition: all 0.3s ease;
|
414 |
+
}
|
415 |
+
|
416 |
+
.theme-card:hover {
|
417 |
+
transform: translateY(-5px);
|
418 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
419 |
+
}
|
420 |
+
|
421 |
+
.theme-header {
|
422 |
+
text-align: center;
|
423 |
+
margin-bottom: 2rem;
|
424 |
+
}
|
425 |
+
|
426 |
+
.theme-header h2 {
|
427 |
+
font-size: 2rem;
|
428 |
+
color: #1e88e5;
|
429 |
+
margin-bottom: 1rem;
|
430 |
+
}
|
431 |
+
|
432 |
+
.theme-header p {
|
433 |
+
color: #666;
|
434 |
+
font-size: 1.1rem;
|
435 |
+
}
|
436 |
+
|
437 |
+
/* ปรับแต่งปุ่มเลือกธีม */
|
438 |
+
.stButton > button {
|
439 |
+
width: 100%;
|
440 |
+
padding: 0.75rem;
|
441 |
+
border: none;
|
442 |
+
border-radius: 8px;
|
443 |
+
font-family: 'Sarabun', sans-serif;
|
444 |
+
transition: all 0.2s ease;
|
445 |
+
}
|
446 |
+
|
447 |
+
.stButton > button:hover {
|
448 |
+
transform: translateY(-2px);
|
449 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
450 |
+
}
|
451 |
+
|
452 |
+
.thai {
|
453 |
+
color: #1e88e5;
|
454 |
+
}
|
455 |
+
|
456 |
+
.eng {
|
457 |
+
color: #333;
|
458 |
+
}
|
459 |
+
|
460 |
+
/* Error Messages */
|
461 |
+
.error-message {
|
462 |
+
color: #d32f2f;
|
463 |
+
padding: 10px;
|
464 |
+
border-left: 4px solid #d32f2f;
|
465 |
+
background-color: #ffebee;
|
466 |
+
margin: 10px 0;
|
467 |
+
}
|
468 |
+
|
469 |
+
/* Success Messages */
|
470 |
+
.success-message {
|
471 |
+
color: #2e7d32;
|
472 |
+
padding: 10px;
|
473 |
+
border-left: 4px solid #2e7d32;
|
474 |
+
background-color: #e8f5e9;
|
475 |
+
margin: 10px 0;
|
476 |
+
}
|
477 |
+
</style>
|
478 |
+
""", unsafe_allow_html=True)
|
479 |
+
|
480 |
+
# Reset counter when page reloads
|
481 |
+
if 'theme_button_counter' in st.session_state:
|
482 |
+
st.session_state.theme_button_counter = 0
|
483 |
+
|
484 |
+
# === 3. STATE MANAGEMENT ===
|
485 |
+
def init_session_state():
|
486 |
+
"""Initialize all session state variables with default values"""
|
487 |
+
|
488 |
+
# Basic states
|
489 |
+
default_states = {
|
490 |
+
'theme_selection_id': datetime.now().strftime('%Y%m%d%H%M%S'),
|
491 |
+
'current_theme': None,
|
492 |
+
'theme_button_counter': 0,
|
493 |
+
'theme_story_starter': None,
|
494 |
+
'story': [],
|
495 |
+
'feedback': None,
|
496 |
+
'level': DEFAULT_LEVEL,
|
497 |
+
'should_reset': False,
|
498 |
+
'last_interaction': datetime.now().isoformat(),
|
499 |
+
}
|
500 |
+
|
501 |
+
# Points system
|
502 |
+
points_states = {
|
503 |
+
'points': {
|
504 |
+
'total': 0,
|
505 |
+
'perfect_sentences': 0,
|
506 |
+
'corrections_made': 0,
|
507 |
+
'streak': 0,
|
508 |
+
'max_streak': 0
|
509 |
+
}
|
510 |
+
}
|
511 |
+
|
512 |
+
# Statistics tracking
|
513 |
+
stats_states = {
|
514 |
+
'stats': {
|
515 |
+
'total_sentences': 0,
|
516 |
+
'correct_first_try': 0,
|
517 |
+
'accuracy_rate': 0.0,
|
518 |
+
'vocabulary_used': set(),
|
519 |
+
'corrections_made': 0,
|
520 |
+
'average_sentence_length': 0,
|
521 |
+
'total_words': 0,
|
522 |
+
'session_duration': 0
|
523 |
+
}
|
524 |
+
}
|
525 |
+
|
526 |
+
# Game progress
|
527 |
+
progress_states = {
|
528 |
+
'achievements': [],
|
529 |
+
'unlocked_features': set(),
|
530 |
+
'current_milestone': 0,
|
531 |
+
'next_milestone': 5
|
532 |
+
}
|
533 |
+
|
534 |
+
# User preferences
|
535 |
+
preferences_states = {
|
536 |
+
'language': 'th',
|
537 |
+
'feedback_level': 'detailed',
|
538 |
+
'theme_color': 'light',
|
539 |
+
'sound_enabled': True
|
540 |
+
}
|
541 |
+
|
542 |
+
# Initialize all states if they don't exist
|
543 |
+
for state_dict in [default_states, points_states, stats_states, progress_states, preferences_states]:
|
544 |
+
for key, value in state_dict.items():
|
545 |
+
if key not in st.session_state:
|
546 |
+
st.session_state[key] = value
|
547 |
+
|
548 |
+
if 'clear_input' not in st.session_state:
|
549 |
+
st.session_state.clear_input = False
|
550 |
+
if 'text_input' not in st.session_state:
|
551 |
+
st.session_state.text_input = ""
|
552 |
+
|
553 |
+
def init_theme_state():
|
554 |
+
"""Initialize theme-specific state variables"""
|
555 |
+
if 'current_theme' not in st.session_state:
|
556 |
+
st.session_state.current_theme = None
|
557 |
+
if 'theme_story_starter' not in st.session_state:
|
558 |
+
st.session_state.theme_story_starter = None
|
559 |
+
|
560 |
+
def reset_story():
|
561 |
+
"""Reset story and related state variables"""
|
562 |
+
try:
|
563 |
+
# Reset story-related states
|
564 |
+
st.session_state.story = []
|
565 |
+
st.session_state.feedback = None
|
566 |
+
st.session_state.theme_story_starter = None
|
567 |
+
|
568 |
+
# Reset points
|
569 |
+
st.session_state.points = {
|
570 |
+
'total': 0,
|
571 |
+
'perfect_sentences': 0,
|
572 |
+
'corrections_made': 0,
|
573 |
+
'streak': 0,
|
574 |
+
'max_streak': 0
|
575 |
+
}
|
576 |
+
|
577 |
+
# Reset stats
|
578 |
+
st.session_state.stats = {
|
579 |
+
'total_sentences': 0,
|
580 |
+
'correct_first_try': 0,
|
581 |
+
'accuracy_rate': 0.0,
|
582 |
+
'vocabulary_used': set(),
|
583 |
+
'corrections_made': 0,
|
584 |
+
'average_sentence_length': 0,
|
585 |
+
'total_words': 0,
|
586 |
+
'session_duration': 0
|
587 |
+
}
|
588 |
+
|
589 |
+
# Reset progress
|
590 |
+
st.session_state.current_milestone = 0
|
591 |
+
st.session_state.next_milestone = 5
|
592 |
+
|
593 |
+
# Reset flag
|
594 |
+
st.session_state.should_reset = False
|
595 |
+
|
596 |
+
# Log reset
|
597 |
+
logging.info("Story state reset successfully")
|
598 |
+
|
599 |
+
return True
|
600 |
+
|
601 |
+
except Exception as e:
|
602 |
+
logging.error(f"Error resetting story state: {str(e)}")
|
603 |
+
st.error("เกิดข้อผิดพลาดในการรีเซ็ตเรื่อง กรุณาลองใหม่อีกครั้ง")
|
604 |
+
return False
|
605 |
+
|
606 |
+
def save_progress() -> Dict:
|
607 |
+
"""Save current progress to JSON format"""
|
608 |
+
try:
|
609 |
+
progress_data = {
|
610 |
+
'timestamp': datetime.now().isoformat(),
|
611 |
+
'level': st.session_state.level,
|
612 |
+
'story': st.session_state.story,
|
613 |
+
'stats': {
|
614 |
+
key: list(value) if isinstance(value, set) else value
|
615 |
+
for key, value in st.session_state.stats.items()
|
616 |
+
},
|
617 |
+
'points': st.session_state.points,
|
618 |
+
'achievements': st.session_state.achievements,
|
619 |
+
'current_theme': st.session_state.current_theme
|
620 |
+
}
|
621 |
+
|
622 |
+
logging.info("Progress saved successfully")
|
623 |
+
return progress_data
|
624 |
+
|
625 |
+
except Exception as e:
|
626 |
+
logging.error(f"Error saving progress: {str(e)}")
|
627 |
+
raise
|
628 |
+
|
629 |
+
def load_progress(data: Dict):
|
630 |
+
"""Load progress from saved data"""
|
631 |
+
try:
|
632 |
+
# Validate data structure
|
633 |
+
required_keys = ['level', 'story', 'stats', 'points', 'achievements']
|
634 |
+
if not all(key in data for key in required_keys):
|
635 |
+
raise ValueError("Invalid save data format")
|
636 |
+
|
637 |
+
# Load basic data
|
638 |
+
st.session_state.level = data['level']
|
639 |
+
st.session_state.story = data['story']
|
640 |
+
st.session_state.achievements = data['achievements']
|
641 |
+
st.session_state.points = data['points']
|
642 |
+
|
643 |
+
# Load stats (converting lists back to sets where needed)
|
644 |
+
st.session_state.stats = {
|
645 |
+
'total_sentences': data['stats']['total_sentences'],
|
646 |
+
'correct_first_try': data['stats']['correct_first_try'],
|
647 |
+
'accuracy_rate': data['stats']['accuracy_rate'],
|
648 |
+
'vocabulary_used': set(data['stats']['vocabulary_used']),
|
649 |
+
'corrections_made': data['stats']['corrections_made'],
|
650 |
+
'average_sentence_length': data['stats'].get('average_sentence_length', 0),
|
651 |
+
'total_words': data['stats'].get('total_words', 0),
|
652 |
+
'session_duration': data['stats'].get('session_duration', 0)
|
653 |
+
}
|
654 |
+
|
655 |
+
# Load theme if present
|
656 |
+
if 'current_theme' in data:
|
657 |
+
st.session_state.current_theme = data['current_theme']
|
658 |
+
|
659 |
+
logging.info("Progress loaded successfully")
|
660 |
+
st.success("โหลดความก้าวหน้าเรียบร้อย!")
|
661 |
+
|
662 |
+
return True
|
663 |
+
|
664 |
+
except Exception as e:
|
665 |
+
logging.error(f"Error loading progress: {str(e)}")
|
666 |
+
st.error("เกิดข้อผิดพลาดในการโหลดข้อมูล กรุณาตรวจสอบไฟล์และลองใหม่อีกครั้ง")
|
667 |
+
return False
|
668 |
+
|
669 |
+
def update_session_stats():
|
670 |
+
"""Update session statistics"""
|
671 |
+
try:
|
672 |
+
if st.session_state.story:
|
673 |
+
# Update word counts
|
674 |
+
all_text = ' '.join([entry['content'] for entry in st.session_state.story])
|
675 |
+
words = all_text.split()
|
676 |
+
st.session_state.stats['total_words'] = len(words)
|
677 |
+
|
678 |
+
# Update average sentence length
|
679 |
+
if st.session_state.stats['total_sentences'] > 0:
|
680 |
+
st.session_state.stats['average_sentence_length'] = (
|
681 |
+
st.session_state.stats['total_words'] /
|
682 |
+
st.session_state.stats['total_sentences']
|
683 |
+
)
|
684 |
+
|
685 |
+
# Update session duration
|
686 |
+
start_time = datetime.fromisoformat(st.session_state.last_interaction)
|
687 |
+
current_time = datetime.now()
|
688 |
+
duration = (current_time - start_time).total_seconds()
|
689 |
+
st.session_state.stats['session_duration'] = duration
|
690 |
+
|
691 |
+
# Update last interaction time
|
692 |
+
st.session_state.last_interaction = current_time.isoformat()
|
693 |
+
|
694 |
+
return True
|
695 |
+
|
696 |
+
except Exception as e:
|
697 |
+
logging.error(f"Error updating session stats: {str(e)}")
|
698 |
+
return False
|
699 |
+
|
700 |
+
# === 4. UTILITY FUNCTIONS ===
|
701 |
+
def generate_story_continuation(user_input: str, level: str) -> str:
|
702 |
+
"""Generate AI story continuation using ChatGPT"""
|
703 |
+
|
704 |
+
level_context = {
|
705 |
+
'Beginner': {
|
706 |
+
'instructions': """
|
707 |
+
Role: Teaching assistant for Thai students (grades 1-3)
|
708 |
+
Rules:
|
709 |
+
- Use only 1-2 VERY simple sentences
|
710 |
+
- Use Present Simple Tense only
|
711 |
+
- Basic vocabulary (family, school, daily activities)
|
712 |
+
- 5-7 words per sentence maximum
|
713 |
+
- Focus on clear, basic responses
|
714 |
+
""",
|
715 |
+
'max_tokens': 30,
|
716 |
+
'temperature': 0.6
|
717 |
+
},
|
718 |
+
'Intermediate': {
|
719 |
+
'instructions': """
|
720 |
+
Role: Teaching assistant for Thai students (grades 4-6)
|
721 |
+
Rules:
|
722 |
+
- Use exactly 2 sentences maximum
|
723 |
+
- Can use Present or Past Tense
|
724 |
+
- Keep each sentence under 12 words
|
725 |
+
- Grade-appropriate vocabulary
|
726 |
+
- Add simple descriptions but stay concise
|
727 |
+
""",
|
728 |
+
'max_tokens': 40,
|
729 |
+
'temperature': 0.7
|
730 |
+
},
|
731 |
+
'Advanced': {
|
732 |
+
'instructions': """
|
733 |
+
Role: Teaching assistant for Thai students (grades 7-9)
|
734 |
+
Rules:
|
735 |
+
- Use 2-3 sentences maximum
|
736 |
+
- Various tenses allowed
|
737 |
+
- Natural sentence length but keep overall response concise
|
738 |
+
- More sophisticated vocabulary and structures
|
739 |
+
- Create engaging responses that encourage creative continuation
|
740 |
+
""",
|
741 |
+
'max_tokens': 50,
|
742 |
+
'temperature': 0.8
|
743 |
+
}
|
744 |
+
}
|
745 |
+
|
746 |
+
try:
|
747 |
+
# Get recent story context
|
748 |
+
story_context = '\n'.join([
|
749 |
+
entry['content'] for entry in st.session_state.story[-3:]
|
750 |
+
]) if st.session_state.story else "Story just started"
|
751 |
+
|
752 |
+
# Create prompt
|
753 |
+
level_settings = level_context[level]
|
754 |
+
|
755 |
+
for _ in range(MAX_RETRIES):
|
756 |
+
try:
|
757 |
+
response = client.chat.completions.create(
|
758 |
+
model="gpt-4o-mini",
|
759 |
+
messages=[
|
760 |
+
{
|
761 |
+
"role": "system",
|
762 |
+
"content": f"""
|
763 |
+
{level_settings['instructions']}
|
764 |
+
|
765 |
+
CRUCIAL GUIDELINES:
|
766 |
+
- Never exceed the maximum sentences for the level
|
767 |
+
- Create openings for student's creativity
|
768 |
+
- Do not resolve plot points or conclude the story
|
769 |
+
- Avoid using 'suddenly' or 'then'
|
770 |
+
- Make each sentence meaningful but incomplete
|
771 |
+
- Leave room for the student to develop the story
|
772 |
+
"""
|
773 |
+
},
|
774 |
+
{
|
775 |
+
"role": "user",
|
776 |
+
"content": f"Story context:\n{story_context}\nStudent's input:\n{user_input}\nProvide a brief continuation:"
|
777 |
+
}
|
778 |
+
],
|
779 |
+
max_tokens=level_settings['max_tokens'],
|
780 |
+
temperature=level_settings['temperature'],
|
781 |
+
presence_penalty=0.6,
|
782 |
+
frequency_penalty=0.6
|
783 |
+
)
|
784 |
+
|
785 |
+
# Process and clean response
|
786 |
+
response_text = response.choices[0].message.content.strip()
|
787 |
+
sentences = [s.strip() for s in response_text.split('.') if s.strip()]
|
788 |
+
|
789 |
+
# Limit sentences based on level
|
790 |
+
max_sentences = {'Beginner': 2, 'Intermediate': 2, 'Advanced': 3}
|
791 |
+
if len(sentences) > max_sentences[level]:
|
792 |
+
sentences = sentences[:max_sentences[level]]
|
793 |
+
|
794 |
+
# Reconstruct response
|
795 |
+
final_response = '. '.join(sentences) + '.'
|
796 |
+
|
797 |
+
logging.info(f"Generated continuation for level {level}")
|
798 |
+
return final_response
|
799 |
+
|
800 |
+
except Exception as e:
|
801 |
+
if _ < MAX_RETRIES - 1:
|
802 |
+
logging.warning(f"Retry {_+1} failed: {str(e)}")
|
803 |
+
continue
|
804 |
+
raise
|
805 |
+
|
806 |
+
raise Exception("Max retries exceeded")
|
807 |
+
|
808 |
+
except Exception as e:
|
809 |
+
logging.error(f"Error generating story continuation: {str(e)}")
|
810 |
+
return "I'm having trouble continuing the story. Please try again."
|
811 |
+
|
812 |
+
def generate_dynamic_story_starter(theme_id: str, level: str) -> Dict[str, str]:
|
813 |
+
"""
|
814 |
+
Dynamically generate a story starter based on theme and level.
|
815 |
+
Returns both Thai and English versions.
|
816 |
+
"""
|
817 |
+
try:
|
818 |
+
# Get theme data
|
819 |
+
theme = story_themes.get(theme_id)
|
820 |
+
if not theme:
|
821 |
+
raise ValueError(f"Theme {theme_id} not found")
|
822 |
+
|
823 |
+
# Get random starter for the level
|
824 |
+
level_starters = theme['story_starters'].get(level, [])
|
825 |
+
if not level_starters:
|
826 |
+
# Fallback to Beginner level if no starters found for specified level
|
827 |
+
level_starters = theme['story_starters'].get('Beginner', [])
|
828 |
+
|
829 |
+
if not level_starters:
|
830 |
+
raise ValueError(f"No story starters found for theme {theme_id}")
|
831 |
+
|
832 |
+
# Select random starter
|
833 |
+
starter = random.choice(level_starters)
|
834 |
+
|
835 |
+
# Return both languages
|
836 |
+
return {
|
837 |
+
'en': starter['en'],
|
838 |
+
'th': starter['th']
|
839 |
+
}
|
840 |
+
|
841 |
+
except Exception as e:
|
842 |
+
logging.error(f"Error generating story starter: {str(e)}")
|
843 |
+
# Provide fallback starter
|
844 |
+
return {
|
845 |
+
'en': 'Once upon a time...',
|
846 |
+
'th': 'กาลครั้งหนึ่ง...'
|
847 |
+
}
|
848 |
+
|
849 |
+
def update_points(is_correct_first_try: bool):
|
850 |
+
"""อัพเดตคะแนนตามผลการเขียน"""
|
851 |
+
try:
|
852 |
+
# คำนวณคะแนนพื้นฐาน
|
853 |
+
base_points = 10
|
854 |
+
|
855 |
+
if is_correct_first_try:
|
856 |
+
# ถูกต้องในครั้งแรก
|
857 |
+
points = base_points * 2
|
858 |
+
st.session_state.points['perfect_sentences'] += 1
|
859 |
+
st.session_state.points['streak'] += 1
|
860 |
+
|
861 |
+
# อัพเดต max streak
|
862 |
+
if st.session_state.points['streak'] > st.session_state.points['max_streak']:
|
863 |
+
st.session_state.points['max_streak'] = st.session_state.points['streak']
|
864 |
+
else:
|
865 |
+
# ต้องแก้ไข
|
866 |
+
points = base_points // 2
|
867 |
+
st.session_state.points['corrections_made'] += 1
|
868 |
+
st.session_state.points['streak'] = 0
|
869 |
+
|
870 |
+
# เพิ่มคะแนนรวม
|
871 |
+
st.session_state.points['total'] += points
|
872 |
+
|
873 |
+
# อัพเดตสถิติ
|
874 |
+
st.session_state.stats['total_sentences'] += 1
|
875 |
+
if is_correct_first_try:
|
876 |
+
st.session_state.stats['correct_first_try'] += 1
|
877 |
+
|
878 |
+
# คำนวณอัตราความถูกต้อง
|
879 |
+
st.session_state.stats['accuracy_rate'] = (
|
880 |
+
st.session_state.stats['correct_first_try'] /
|
881 |
+
st.session_state.stats['total_sentences'] * 100
|
882 |
+
)
|
883 |
+
|
884 |
+
logging.info(f"Points updated: +{points} points")
|
885 |
+
return True
|
886 |
+
|
887 |
+
except Exception as e:
|
888 |
+
logging.error(f"Error updating points: {str(e)}")
|
889 |
+
return False
|
890 |
+
|
891 |
+
def apply_correction(story_index: int, corrected_text: str):
|
892 |
+
"""แก้ไขประโยคในเรื่อง"""
|
893 |
+
try:
|
894 |
+
if not (0 <= story_index < len(st.session_state.story)):
|
895 |
+
raise ValueError("Invalid story index")
|
896 |
+
|
897 |
+
# เก็บข้อความเดิม
|
898 |
+
original_text = st.session_state.story[story_index]['content']
|
899 |
+
|
900 |
+
# อัพเดทข้อความ
|
901 |
+
st.session_state.story[story_index].update({
|
902 |
+
'content': corrected_text,
|
903 |
+
'is_corrected': True,
|
904 |
+
'is_correct': True,
|
905 |
+
'original_text': original_text,
|
906 |
+
'correction_timestamp': datetime.now().isoformat()
|
907 |
+
})
|
908 |
+
|
909 |
+
# อัพเดทสถิติ
|
910 |
+
st.session_state.stats['corrections_made'] += 1
|
911 |
+
|
912 |
+
# Log การแก้ไข
|
913 |
+
logging.info(f"Sentence corrected at index {story_index}")
|
914 |
+
|
915 |
+
# แสดงข้อความยืนยัน
|
916 |
+
st.success("✅ แก้ไขประโยคเรียบร้อยแล้ว!")
|
917 |
+
return True
|
918 |
+
|
919 |
+
except Exception as e:
|
920 |
+
logging.error(f"Error applying correction: {str(e)}")
|
921 |
+
st.error("เกิดข้อผิดพลาดในการแก้ไขประโยค กรุณาลองใหม่อีกครั้ง")
|
922 |
+
return False
|
923 |
+
|
924 |
+
def update_achievements():
|
925 |
+
"""ตรวจสอบและอัพเดตความสำเร็จ"""
|
926 |
+
try:
|
927 |
+
current_achievements = st.session_state.achievements
|
928 |
+
|
929 |
+
# ตรวจสอบเงื่อนไขต่างๆ
|
930 |
+
if (st.session_state.points['streak'] >= 5 and
|
931 |
+
"🌟 นักเขียนไร้ที่ติ" not in current_achievements):
|
932 |
+
current_achievements.append("🌟 นักเขียนไร้ที่ติ")
|
933 |
+
st.success("🎉 ได้รับความสำเร็จใหม่: นักเขียนไร้ที่ติ!")
|
934 |
+
|
935 |
+
if (len(st.session_state.stats['vocabulary_used']) >= 50 and
|
936 |
+
"📚 ราชาคำศัพท์" not in current_achievements):
|
937 |
+
current_achievements.append("📚 ราชาคำศัพท์")
|
938 |
+
st.success("🎉 ได้รับความสำเร็จใหม่: ราชาคำศัพท์!")
|
939 |
+
|
940 |
+
if (len(st.session_state.story) >= 10 and
|
941 |
+
"📖 นักแต่งนิทาน" not in current_achievements):
|
942 |
+
current_achievements.append("📖 นักแต่งนิทาน")
|
943 |
+
st.success("🎉 ได้รับความสำเร็จใหม่: นักแต่งนิทาน!")
|
944 |
+
|
945 |
+
if (st.session_state.stats['total_sentences'] >= 10 and
|
946 |
+
st.session_state.stats['accuracy_rate'] >= 80 and
|
947 |
+
"👑 ราชาความแม่นยำ" not in current_achievements):
|
948 |
+
current_achievements.append("👑 ราชาความแม่นยำ")
|
949 |
+
st.success("🎉 ได้รับความสำเร็จใหม่: ราชาความแม่นยำ!")
|
950 |
+
|
951 |
+
# บันทึกความสำเร็จ
|
952 |
+
st.session_state.achievements = current_achievements
|
953 |
+
logging.info("Achievements updated successfully")
|
954 |
+
return True
|
955 |
+
|
956 |
+
except Exception as e:
|
957 |
+
logging.error(f"Error updating achievements: {str(e)}")
|
958 |
+
return False
|
959 |
+
|
960 |
+
def provide_feedback(text: str, level: str) -> Dict[str, str]:
|
961 |
+
"""Provide feedback on the user's writing with appropriate level"""
|
962 |
+
|
963 |
+
level_prompts = {
|
964 |
+
'Beginner': """
|
965 |
+
Focus on:
|
966 |
+
- Basic sentence structure (Subject + Verb + Object)
|
967 |
+
- Present Simple Tense usage
|
968 |
+
- Basic vocabulary
|
969 |
+
- Capitalization and periods
|
970 |
+
Provide very simple, encouraging feedback in Thai.
|
971 |
+
""",
|
972 |
+
'Intermediate': """
|
973 |
+
Focus on:
|
974 |
+
- Sentence variety
|
975 |
+
- Past Tense usage
|
976 |
+
- Vocabulary appropriateness
|
977 |
+
- Basic punctuation
|
978 |
+
- Simple conjunctions
|
979 |
+
Provide moderately detailed feedback in Thai.
|
980 |
+
""",
|
981 |
+
'Advanced': """
|
982 |
+
Focus on:
|
983 |
+
- Complex sentence structures
|
984 |
+
- Various tense usage
|
985 |
+
- Advanced vocabulary
|
986 |
+
- All punctuation
|
987 |
+
- Style and flow
|
988 |
+
Provide comprehensive feedback in Thai.
|
989 |
+
"""
|
990 |
+
}
|
991 |
+
|
992 |
+
try:
|
993 |
+
for _ in range(MAX_RETRIES):
|
994 |
+
try:
|
995 |
+
response = client.chat.completions.create(
|
996 |
+
model="gpt-4o-mini",
|
997 |
+
messages=[
|
998 |
+
{
|
999 |
+
"role": "system",
|
1000 |
+
"content": f"""
|
1001 |
+
You are a Thai English teacher helping {level} students.
|
1002 |
+
{level_prompts[level]}
|
1003 |
+
|
1004 |
+
Return your response in this EXACT format (valid JSON):
|
1005 |
+
{{
|
1006 |
+
"feedback": "(ข้อเสนอแนะภาษาไทย)",
|
1007 |
+
"corrected": "(ประโยคภาษาอังกฤษที่ถูกต้อง)",
|
1008 |
+
"has_errors": true/false,
|
1009 |
+
"error_types": ["grammar", "vocabulary", "spelling", ...],
|
1010 |
+
"difficulty_score": 1-10
|
1011 |
+
}}
|
1012 |
+
"""
|
1013 |
+
},
|
1014 |
+
{
|
1015 |
+
"role": "user",
|
1016 |
+
"content": f"Review this sentence: {text}"
|
1017 |
+
}
|
1018 |
+
],
|
1019 |
+
max_tokens=200,
|
1020 |
+
temperature=0.3
|
1021 |
+
)
|
1022 |
+
|
1023 |
+
# Parse response
|
1024 |
+
feedback_data = json.loads(response.choices[0].message.content.strip())
|
1025 |
+
|
1026 |
+
# Validate required fields
|
1027 |
+
required_fields = ['feedback', 'corrected', 'has_errors']
|
1028 |
+
if not all(field in feedback_data for field in required_fields):
|
1029 |
+
raise ValueError("Missing required fields in feedback")
|
1030 |
+
|
1031 |
+
logging.info(f"Generated feedback for level {level}")
|
1032 |
+
return feedback_data
|
1033 |
+
|
1034 |
+
except json.JSONDecodeError:
|
1035 |
+
if _ < MAX_RETRIES - 1:
|
1036 |
+
continue
|
1037 |
+
raise
|
1038 |
+
|
1039 |
+
raise Exception("Max retries exceeded")
|
1040 |
+
|
1041 |
+
except Exception as e:
|
1042 |
+
logging.error(f"Error generating feedback: {str(e)}")
|
1043 |
+
return {
|
1044 |
+
"feedback": "⚠️ ระบบไม่สามารถวิเคราะห์ประโยคได้ กรุณาลองใหม่อีกครั้ง",
|
1045 |
+
"corrected": text,
|
1046 |
+
"has_errors": False,
|
1047 |
+
"error_types": [],
|
1048 |
+
"difficulty_score": 5
|
1049 |
+
}
|
1050 |
+
|
1051 |
+
def get_vocabulary_suggestions(context: str = "", level: str = DEFAULT_LEVEL) -> List[str]:
|
1052 |
+
"""Get contextual vocabulary suggestions with Thai translations"""
|
1053 |
+
try:
|
1054 |
+
recent_story = context or '\n'.join([
|
1055 |
+
entry['content'] for entry in st.session_state.story[-3:]
|
1056 |
+
]) if st.session_state.story else "Story just started"
|
1057 |
+
|
1058 |
+
response = client.chat.completions.create(
|
1059 |
+
model="gpt-4o-mini",
|
1060 |
+
messages=[
|
1061 |
+
{
|
1062 |
+
"role": "system",
|
1063 |
+
"content": f"""
|
1064 |
+
You are a Thai-English bilingual teacher.
|
1065 |
+
Suggest 5 English words appropriate for {level} level students.
|
1066 |
+
Format each suggestion as:
|
1067 |
+
word (type) - คำแปล | example sentence
|
1068 |
+
"""
|
1069 |
+
},
|
1070 |
+
{
|
1071 |
+
"role": "user",
|
1072 |
+
"content": f"Story context:\n{recent_story}\n\nSuggest relevant words:"
|
1073 |
+
}
|
1074 |
+
],
|
1075 |
+
max_tokens=200,
|
1076 |
+
temperature=0.8
|
1077 |
+
)
|
1078 |
+
|
1079 |
+
suggestions = response.choices[0].message.content.split('\n')
|
1080 |
+
return [s.strip() for s in suggestions if s.strip()]
|
1081 |
+
|
1082 |
+
except Exception as e:
|
1083 |
+
logging.error(f"Error getting vocabulary suggestions: {str(e)}")
|
1084 |
+
return [
|
1085 |
+
"happy (adj) - มีความสุข | I am happy today",
|
1086 |
+
"run (verb) - วิ่ง | The dog runs fast",
|
1087 |
+
"tree (noun) - ต้นไม้ | A tall tree"
|
1088 |
+
]
|
1089 |
+
|
1090 |
+
def get_creative_prompt() -> Dict[str, str]:
|
1091 |
+
"""Generate a bilingual creative writing prompt"""
|
1092 |
+
try:
|
1093 |
+
response = client.chat.completions.create(
|
1094 |
+
model="gpt-4o-mini",
|
1095 |
+
messages=[
|
1096 |
+
{
|
1097 |
+
"role": "system",
|
1098 |
+
"content": """
|
1099 |
+
Create short story prompts in both English and Thai.
|
1100 |
+
Keep it simple and under 6 words each.
|
1101 |
+
Make it open-ended and encouraging.
|
1102 |
+
"""
|
1103 |
+
},
|
1104 |
+
{
|
1105 |
+
"role": "user",
|
1106 |
+
"content": "Generate a creative writing prompt:"
|
1107 |
+
}
|
1108 |
+
],
|
1109 |
+
max_tokens=50,
|
1110 |
+
temperature=0.7
|
1111 |
+
)
|
1112 |
+
|
1113 |
+
prompt_eng = response.choices[0].message.content.strip()
|
1114 |
+
|
1115 |
+
# Get Thai translation
|
1116 |
+
response_thai = client.chat.completions.create(
|
1117 |
+
model="gpt-4o-mini",
|
1118 |
+
messages=[
|
1119 |
+
{
|
1120 |
+
"role": "system",
|
1121 |
+
"content": "Translate to short Thai prompt, keep it natural:"
|
1122 |
+
},
|
1123 |
+
{
|
1124 |
+
"role": "user",
|
1125 |
+
"content": prompt_eng
|
1126 |
+
}
|
1127 |
+
],
|
1128 |
+
max_tokens=50,
|
1129 |
+
temperature=0.7
|
1130 |
+
)
|
1131 |
+
|
1132 |
+
prompt_thai = response_thai.choices[0].message.content.strip()
|
1133 |
+
|
1134 |
+
return {
|
1135 |
+
"eng": prompt_eng,
|
1136 |
+
"thai": prompt_thai
|
1137 |
+
}
|
1138 |
+
|
1139 |
+
except Exception as e:
|
1140 |
+
logging.error(f"Error generating creative prompt: {str(e)}")
|
1141 |
+
return {
|
1142 |
+
"eng": "What happens next?",
|
1143 |
+
"thai": "แล้วอะไรจะเกิดขึ้นต่อ?"
|
1144 |
+
}
|
1145 |
+
|
1146 |
+
def calculate_text_metrics(text: str) -> Dict[str, float]:
|
1147 |
+
"""Calculate various metrics for the given text"""
|
1148 |
+
try:
|
1149 |
+
words = text.split()
|
1150 |
+
sentences = [s.strip() for s in text.split('.') if s.strip()]
|
1151 |
+
|
1152 |
+
metrics = {
|
1153 |
+
'word_count': len(words),
|
1154 |
+
'sentence_count': len(sentences),
|
1155 |
+
'average_word_length': sum(len(word) for word in words) / len(words) if words else 0,
|
1156 |
+
'unique_words': len(set(words)),
|
1157 |
+
'complexity_score': calculate_complexity_score(text)
|
1158 |
+
}
|
1159 |
+
|
1160 |
+
return metrics
|
1161 |
+
|
1162 |
+
except Exception as e:
|
1163 |
+
logging.error(f"Error calculating text metrics: {str(e)}")
|
1164 |
+
return {
|
1165 |
+
'word_count': 0,
|
1166 |
+
'sentence_count': 0,
|
1167 |
+
'average_word_length': 0,
|
1168 |
+
'unique_words': 0,
|
1169 |
+
'complexity_score': 0
|
1170 |
+
}
|
1171 |
+
|
1172 |
+
def calculate_complexity_score(text: str) -> float:
|
1173 |
+
"""Calculate a complexity score for the text (0-10)"""
|
1174 |
+
try:
|
1175 |
+
# Basic metrics
|
1176 |
+
words = text.split()
|
1177 |
+
word_count = len(words)
|
1178 |
+
unique_words = len(set(words))
|
1179 |
+
avg_word_length = sum(len(word) for word in words) / word_count if word_count > 0 else 0
|
1180 |
+
|
1181 |
+
# Calculate score components
|
1182 |
+
vocabulary_score = (unique_words / word_count) * 5 if word_count > 0 else 0
|
1183 |
+
length_score = min((avg_word_length / 10) * 5, 5)
|
1184 |
+
|
1185 |
+
# Combine scores
|
1186 |
+
total_score = vocabulary_score + length_score
|
1187 |
+
|
1188 |
+
return min(total_score, 10.0)
|
1189 |
+
|
1190 |
+
except Exception as e:
|
1191 |
+
logging.error(f"Error calculating complexity score: {str(e)}")
|
1192 |
+
return 5.0
|
1193 |
+
|
1194 |
+
# === 5. UI COMPONENTS ===
|
1195 |
+
def show_welcome_section():
|
1196 |
+
"""Display welcome message and introduction"""
|
1197 |
+
st.markdown("""
|
1198 |
+
<div class="welcome-header" style="text-align: center; padding: 20px;">
|
1199 |
+
<div class="thai" style="font-size: 1.5em; margin-bottom: 10px;">
|
1200 |
+
🌟 ยินดีต้อนรับสู่ JoyStory - แอพฝึกเขียนภาษาอังกฤษแสนสนุก!
|
1201 |
+
</div>
|
1202 |
+
<div style="color: #666; margin-bottom: 20px;">
|
1203 |
+
เรียนรู้ภาษาอังกฤษผ่านการเขียนเรื่องราวด้วยตัวเอง
|
1204 |
+
พร้อมผู้ช่วย AI ที่จะช่วยแนะนำและให้กำลังใจ
|
1205 |
+
</div>
|
1206 |
+
<div class="eng" style="font-style: italic; color: #1e88e5;">
|
1207 |
+
Welcome to JoyStory - Fun English Writing Adventure!
|
1208 |
+
</div>
|
1209 |
+
</div>
|
1210 |
+
""", unsafe_allow_html=True)
|
1211 |
+
|
1212 |
+
def show_parent_guide():
|
1213 |
+
"""Display guide for parents"""
|
1214 |
+
with st.expander("📖 คำแนะนำสำหรับผู้ปกครอง | Parent's Guide"):
|
1215 |
+
st.markdown("""
|
1216 |
+
<div class="parent-guide" style="background-color: #fff3e0; padding: 20px; border-radius: 10px;">
|
1217 |
+
<h4 style="color: #f57c00; margin-bottom: 15px;">คำแนะนำในการใช้งาน</h4>
|
1218 |
+
<ul style="list-style-type: none; padding-left: 0;">
|
1219 |
+
<li style="margin-bottom: 10px;">
|
1220 |
+
👥 <strong>การมีส่วนร่วม:</strong> แนะนำให้นั่งเขียนเรื่องราวร่วมกับน้องๆ
|
1221 |
+
</li>
|
1222 |
+
<li style="margin-bottom: 10px;">
|
1223 |
+
💡 <strong>การช่วยเหลือ:</strong> ช่วยอธิบายคำแนะนำและคำศัพท์ที่น้องๆ ไม่เข้าใจ
|
1224 |
+
</li>
|
1225 |
+
<li style="margin-bottom: 10px;">
|
1226 |
+
🌟 <strong>การให้กำลังใจ:</strong> ให้กำลังใจและชื่นชมเมื่อน้องๆ เขียนได้ดี
|
1227 |
+
</li>
|
1228 |
+
<li style="margin-bottom: 10px;">
|
1229 |
+
⏱️ <strong>เวลาที่เหมาะสม:</strong> ใช้เวลาในการเขียนแต่ละครั้งไม่เกิน 20-30 นาที
|
1230 |
+
</li>
|
1231 |
+
</ul>
|
1232 |
+
<div style="background-color: #ffe0b2; padding: 15px; border-radius: 8px; margin-top: 15px;">
|
1233 |
+
<p style="margin: 0;">
|
1234 |
+
💡 <strong>เคล็ดลับ:</strong> ให้น้องๆ พูดเรื่องราวที่อยากเขียนเป็นภาษาไทยก่อน
|
1235 |
+
แล้วค่อยๆ ช่วยกันแปลงเป็นภาษาอังกฤษ
|
1236 |
+
</p>
|
1237 |
+
</div>
|
1238 |
+
</div>
|
1239 |
+
""", unsafe_allow_html=True)
|
1240 |
+
|
1241 |
+
def show_level_selection():
|
1242 |
+
"""Display level selection interface"""
|
1243 |
+
st.markdown("""
|
1244 |
+
<div class="thai" style="margin-bottom: 15px;">
|
1245 |
+
🎯 เลือกระดับการเรียนรู้
|
1246 |
+
</div>
|
1247 |
+
""", unsafe_allow_html=True)
|
1248 |
+
|
1249 |
+
level = st.radio(
|
1250 |
+
"ระดับการเรียน",
|
1251 |
+
options=list(level_options.keys()),
|
1252 |
+
format_func=lambda x: level_options[x]['thai_name'],
|
1253 |
+
key="level_selector",
|
1254 |
+
label_visibility="collapsed"
|
1255 |
+
)
|
1256 |
+
|
1257 |
+
# Show level details
|
1258 |
+
st.markdown(f"""
|
1259 |
+
<div class="level-info" style="background-color: #e3f2fd; padding: 15px; border-radius: 10px; margin: 15px 0;">
|
1260 |
+
<h4 style="color: #1976d2; margin-bottom: 10px;">
|
1261 |
+
{level_options[level]['thai_name']}
|
1262 |
+
</h4>
|
1263 |
+
<p style="color: #444; margin-bottom: 10px;">
|
1264 |
+
🎯 {level_options[level]['description']}
|
1265 |
+
</p>
|
1266 |
+
<p style="color: #666;">
|
1267 |
+
👥 เหมาะสำหรับอายุ: {level_options[level]['age_range']}
|
1268 |
+
</p>
|
1269 |
+
<div style="margin-top: 15px;">
|
1270 |
+
<h5 style="color: #1976d2; margin-bottom: 8px;">✨ คุณลักษณะ:</h5>
|
1271 |
+
<ul style="list-style-type: none; padding-left: 0;">
|
1272 |
+
{' '.join([f'<li style="margin-bottom: 5px;">• {feature}</li>' for feature in level_options[level]['features']])}
|
1273 |
+
</ul>
|
1274 |
+
</div>
|
1275 |
+
</div>
|
1276 |
+
""", unsafe_allow_html=True)
|
1277 |
+
|
1278 |
+
return level
|
1279 |
+
|
1280 |
+
def show_theme_selection():
|
1281 |
+
"""Display theme selection interface"""
|
1282 |
+
# Header section
|
1283 |
+
header_html = '''
|
1284 |
+
<div style="text-align: center; margin-bottom: 2rem;">
|
1285 |
+
<h2 style="color: #1e88e5; font-family: 'Sarabun', sans-serif; margin: 0; display: flex; align-items: center; justify-content: center; gap: 8px;">
|
1286 |
+
<span style="font-size: 1.5em;">🎨</span>
|
1287 |
+
<span>เลือกธีมเรื่องราว | Choose Story Theme</span>
|
1288 |
+
</h2>
|
1289 |
+
<p style="color: #666; max-width: 600px; margin: 1rem auto 0;">
|
1290 |
+
เลือกโลกแห่งจินตนาการที่คุณต้องการผจญภัย และเริ่มต้นเขียนเรื่องราวของคุณ
|
1291 |
+
</p>
|
1292 |
+
</div>
|
1293 |
+
'''
|
1294 |
+
st.markdown(header_html, unsafe_allow_html=True)
|
1295 |
+
|
1296 |
+
# Filter available themes
|
1297 |
+
available_themes = [
|
1298 |
+
theme for theme in story_themes.values()
|
1299 |
+
if st.session_state.level in theme['level_range']
|
1300 |
+
]
|
1301 |
+
|
1302 |
+
# Create grid layout
|
1303 |
+
num_themes = len(available_themes)
|
1304 |
+
rows = (num_themes + 3) // 4 # Ceiling division to get number of rows needed
|
1305 |
+
|
1306 |
+
# Create rows with 4 themes each
|
1307 |
+
for row in range(rows):
|
1308 |
+
cols = st.columns(4)
|
1309 |
+
for col_idx, col in enumerate(cols):
|
1310 |
+
theme_idx = row * 4 + col_idx
|
1311 |
+
if theme_idx < num_themes:
|
1312 |
+
theme = available_themes[theme_idx]
|
1313 |
+
with col:
|
1314 |
+
# Theme card container
|
1315 |
+
theme_card = f'''
|
1316 |
+
<div style="
|
1317 |
+
background-color: {theme['background_color']};
|
1318 |
+
border-radius: 15px;
|
1319 |
+
padding: 1.5rem;
|
1320 |
+
margin-bottom: 1rem;
|
1321 |
+
height: 100%;
|
1322 |
+
position: relative;
|
1323 |
+
transition: all 0.3s ease;
|
1324 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
1325 |
+
">
|
1326 |
+
<div style="position: absolute; top: 1rem; right: 1rem;
|
1327 |
+
padding: 0.25rem 0.75rem; border-radius: 999px;
|
1328 |
+
font-size: 0.8rem; background: #E3F2FD;
|
1329 |
+
color: #1E88E5;">
|
1330 |
+
{st.session_state.level}
|
1331 |
+
</div>
|
1332 |
+
<div style="font-size: 2.5rem; margin-bottom: 1rem;">
|
1333 |
+
{theme['icon']}
|
1334 |
+
</div>
|
1335 |
+
<div style="font-size: 1.2rem; font-weight: 600;
|
1336 |
+
margin-bottom: 0.5rem; color: {theme['accent_color']};">
|
1337 |
+
{theme['name_th']}
|
1338 |
+
</div>
|
1339 |
+
<div style="font-size: 0.9rem; color: #666;
|
1340 |
+
line-height: 1.4; margin-bottom: 1rem;">
|
1341 |
+
{theme['description_th']}
|
1342 |
+
</div>
|
1343 |
+
</div>
|
1344 |
+
'''
|
1345 |
+
st.markdown(theme_card, unsafe_allow_html=True)
|
1346 |
+
|
1347 |
+
# Theme selection button
|
1348 |
+
if st.button(
|
1349 |
+
f"เลือกธีม {theme['name_th']}",
|
1350 |
+
key=f"theme_{theme['id']}_{row}_{col_idx}",
|
1351 |
+
use_container_width=True
|
1352 |
+
):
|
1353 |
+
handle_theme_selection(theme)
|
1354 |
+
|
1355 |
+
def handle_theme_selection(theme: dict):
|
1356 |
+
"""Handle theme selection and initialization"""
|
1357 |
+
try:
|
1358 |
+
with st.spinner("กำลังเตรียมเรื่องราว..."):
|
1359 |
+
st.session_state.current_theme = theme['id']
|
1360 |
+
starter = generate_dynamic_story_starter(
|
1361 |
+
theme['id'],
|
1362 |
+
st.session_state.level
|
1363 |
+
)
|
1364 |
+
st.session_state.story = [{
|
1365 |
+
"role": "AI",
|
1366 |
+
"content": starter['en'],
|
1367 |
+
"thai_content": starter['th'],
|
1368 |
+
"is_starter": True
|
1369 |
+
}]
|
1370 |
+
st.success(f"เลือกธีม {theme['name_th']} เรียบร้อยแล้ว!")
|
1371 |
+
st.rerun()
|
1372 |
+
except Exception as e:
|
1373 |
+
logging.error(f"Error selecting theme: {str(e)}")
|
1374 |
+
st.error("เกิดข้อผิดพลาดในการเลือกธีม กรุณาลองใหม่อีกครั้ง")
|
1375 |
+
|
1376 |
+
def show_theme_card(theme: Dict): # เปลี่ยนจาก display_theme_card เป็น show_theme_card
|
1377 |
+
"""Display a single theme card with proper styling"""
|
1378 |
+
card_html = f"""
|
1379 |
+
<div style="
|
1380 |
+
background-color: {theme['background_color']};
|
1381 |
+
border-radius: 15px;
|
1382 |
+
padding: 1.5rem;
|
1383 |
+
margin-bottom: 1rem;
|
1384 |
+
height: 100%;
|
1385 |
+
position: relative;
|
1386 |
+
transition: all 0.3s ease;
|
1387 |
+
">
|
1388 |
+
<div style="position: absolute; top: 1rem; right: 1rem; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; background: #E3F2FD; color: #1E88E5;">
|
1389 |
+
{st.session_state.level}
|
1390 |
+
</div>
|
1391 |
+
|
1392 |
+
<div style="font-size: 2.5rem; margin-bottom: 1rem;">
|
1393 |
+
{theme['icon']}
|
1394 |
+
</div>
|
1395 |
+
|
1396 |
+
<div style="font-size: 1.2rem; font-weight: 600; margin-bottom: 0.5rem; color: {theme['accent_color']};">
|
1397 |
+
{theme['name_th']}
|
1398 |
+
</div>
|
1399 |
+
|
1400 |
+
<div style="font-size: 0.9rem; color: #666; line-height: 1.4; margin-bottom: 1rem;">
|
1401 |
+
{theme['description_th']}
|
1402 |
+
</div>
|
1403 |
+
</div>
|
1404 |
+
"""
|
1405 |
+
|
1406 |
+
# แสดงการ์ดธีม
|
1407 |
+
st.markdown(card_html, unsafe_allow_html=True)
|
1408 |
+
|
1409 |
+
# แสดงปุ่มเลือกธีม
|
1410 |
+
if st.button(
|
1411 |
+
f"เลือกธีม {theme['name_th']}",
|
1412 |
+
key=f"theme_{theme['id']}",
|
1413 |
+
help=f"คลิกเพื่อเริ่มเขียนเรื่องราวในธีม {theme['name_th']}",
|
1414 |
+
use_container_width=True
|
1415 |
+
):
|
1416 |
+
try:
|
1417 |
+
with st.spinner("กำลังเตรียมเรื่องราว..."):
|
1418 |
+
st.session_state.current_theme = theme['id']
|
1419 |
+
starter = generate_dynamic_story_starter(
|
1420 |
+
theme['id'],
|
1421 |
+
st.session_state.level
|
1422 |
+
)
|
1423 |
+
st.session_state.story = [{
|
1424 |
+
"role": "AI",
|
1425 |
+
"content": starter['en'],
|
1426 |
+
"thai_content": starter['th'],
|
1427 |
+
"is_starter": True
|
1428 |
+
}]
|
1429 |
+
st.success(f"เลือกธีม {theme['name_th']} เรียบร้อยแล้ว!")
|
1430 |
+
st.rerun()
|
1431 |
+
except Exception as e:
|
1432 |
+
logging.error(f"Error selecting theme: {str(e)}")
|
1433 |
+
st.error("เกิดข้อผิดพลาดในการเลือกธีม กรุณาลองใหม่อีกครั้ง")
|
1434 |
+
|
1435 |
+
def show_story():
|
1436 |
+
"""Display the story with proper formatting"""
|
1437 |
+
story_display = st.container()
|
1438 |
+
|
1439 |
+
with story_display:
|
1440 |
+
if not st.session_state.story:
|
1441 |
+
st.info("เลือกธีมเรื่องราวที่ต้องการเพื่อเริ่มต้นการผจญภัย!")
|
1442 |
+
return
|
1443 |
+
|
1444 |
+
# Story Display Box
|
1445 |
+
for idx, entry in enumerate(st.session_state.story):
|
1446 |
+
if entry['role'] == 'AI':
|
1447 |
+
if entry.get('is_starter'):
|
1448 |
+
# Story Starter
|
1449 |
+
st.markdown(f"""
|
1450 |
+
<div style="
|
1451 |
+
background-color: #f0f7ff;
|
1452 |
+
padding: 20px;
|
1453 |
+
border-radius: 10px;
|
1454 |
+
margin: 10px 0;
|
1455 |
+
border-left: 4px solid #1e88e5;
|
1456 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
1457 |
+
">
|
1458 |
+
<div style="color: #1e88e5; margin-bottom: 10px; font-size: 1.1em;">
|
1459 |
+
🎬 เริ่มเรื่อง:
|
1460 |
+
</div>
|
1461 |
+
<div style="color: #666; margin-bottom: 8px; font-family: 'Sarabun', sans-serif;">
|
1462 |
+
{entry.get('thai_content', '')}
|
1463 |
+
</div>
|
1464 |
+
<div style="color: #333; font-size: 1.1em;">
|
1465 |
+
{entry['content']}
|
1466 |
+
</div>
|
1467 |
+
</div>
|
1468 |
+
""", unsafe_allow_html=True)
|
1469 |
+
else:
|
1470 |
+
# AI Response
|
1471 |
+
st.markdown(f"""
|
1472 |
+
<div style="
|
1473 |
+
background-color: #f8f9fa;
|
1474 |
+
padding: 15px;
|
1475 |
+
border-radius: 8px;
|
1476 |
+
margin: 8px 0;
|
1477 |
+
border-left: 4px solid #4caf50;
|
1478 |
+
">
|
1479 |
+
<div style="color: #2e7d32;">
|
1480 |
+
🤖 AI: {entry['content']}
|
1481 |
+
</div>
|
1482 |
+
</div>
|
1483 |
+
""", unsafe_allow_html=True)
|
1484 |
+
|
1485 |
+
elif entry['role'] == 'You':
|
1486 |
+
# User Entry
|
1487 |
+
status_icon = "✅" if entry.get('is_correct') else "✍️"
|
1488 |
+
bg_color = "#e8f5e9" if entry.get('is_correct') else "#fff"
|
1489 |
+
border_color = "#4caf50" if entry.get('is_correct') else "#1e88e5"
|
1490 |
+
|
1491 |
+
st.markdown(f"""
|
1492 |
+
<div style="
|
1493 |
+
background-color: {bg_color};
|
1494 |
+
padding: 15px;
|
1495 |
+
border-radius: 8px;
|
1496 |
+
margin: 8px 0;
|
1497 |
+
border-left: 4px solid {border_color};
|
1498 |
+
">
|
1499 |
+
<div style="color: #333;">
|
1500 |
+
👤 You: {status_icon} {entry['content']}
|
1501 |
+
</div>
|
1502 |
+
{f'<div style="font-size: 0.9em; color: #666; margin-top: 5px;">{entry.get("feedback", "")}</div>' if entry.get('feedback') else ''}
|
1503 |
+
</div>
|
1504 |
+
""", unsafe_allow_html=True)
|
1505 |
+
|
1506 |
+
def show_feedback_section():
|
1507 |
+
"""Display writing feedback section"""
|
1508 |
+
if not st.session_state.feedback:
|
1509 |
+
return
|
1510 |
+
|
1511 |
+
st.markdown("""
|
1512 |
+
<div style="margin-bottom: 20px;">
|
1513 |
+
<div class="thai-eng">
|
1514 |
+
<div class="thai">📝 คำแนะนำจากครู</div>
|
1515 |
+
<div class="eng">Writing Feedback</div>
|
1516 |
+
</div>
|
1517 |
+
</div>
|
1518 |
+
""", unsafe_allow_html=True)
|
1519 |
+
|
1520 |
+
feedback_data = st.session_state.feedback
|
1521 |
+
if isinstance(feedback_data, dict) and feedback_data.get('has_errors'):
|
1522 |
+
# Show error feedback
|
1523 |
+
st.markdown(f"""
|
1524 |
+
<div style="
|
1525 |
+
background-color: #fff3e0;
|
1526 |
+
padding: 20px;
|
1527 |
+
border-radius: 10px;
|
1528 |
+
border-left: 4px solid #ff9800;
|
1529 |
+
margin: 10px 0;
|
1530 |
+
">
|
1531 |
+
<p style="color: #e65100; margin-bottom: 15px;">
|
1532 |
+
{feedback_data['feedback']}
|
1533 |
+
</p>
|
1534 |
+
<div style="
|
1535 |
+
background-color: #fff;
|
1536 |
+
padding: 15px;
|
1537 |
+
border-radius: 8px;
|
1538 |
+
margin-top: 10px;
|
1539 |
+
">
|
1540 |
+
<p style="color: #666; margin-bottom: 5px;">
|
1541 |
+
ประโยคที่ถูกต้อง:
|
1542 |
+
</p>
|
1543 |
+
<p style="
|
1544 |
+
color: #2e7d32;
|
1545 |
+
font-weight: 500;
|
1546 |
+
font-size: 1.1em;
|
1547 |
+
margin: 0;
|
1548 |
+
">
|
1549 |
+
{feedback_data['corrected']}
|
1550 |
+
</p>
|
1551 |
+
</div>
|
1552 |
+
</div>
|
1553 |
+
""", unsafe_allow_html=True)
|
1554 |
+
|
1555 |
+
# Correction button
|
1556 |
+
if st.button(
|
1557 |
+
"✍️ แก้ไขประโยคให้ถูกต้อง",
|
1558 |
+
key="correct_button",
|
1559 |
+
help="คลิกเพื่อแก้ไขประโยคให้ถูกต้องตามคำแนะนำ"
|
1560 |
+
):
|
1561 |
+
last_user_entry_idx = next(
|
1562 |
+
(i for i, entry in reversed(list(enumerate(st.session_state.story)))
|
1563 |
+
if entry['role'] == 'You'),
|
1564 |
+
None
|
1565 |
+
)
|
1566 |
+
if last_user_entry_idx is not None:
|
1567 |
+
apply_correction(last_user_entry_idx, feedback_data['corrected'])
|
1568 |
+
st.rerun()
|
1569 |
+
else:
|
1570 |
+
# Show success feedback
|
1571 |
+
st.markdown(f"""
|
1572 |
+
<div style="
|
1573 |
+
background-color: #e8f5e9;
|
1574 |
+
padding: 20px;
|
1575 |
+
border-radius: 10px;
|
1576 |
+
border-left: 4px solid #4caf50;
|
1577 |
+
margin: 10px 0;
|
1578 |
+
">
|
1579 |
+
<p style="color: #2e7d32; margin: 0;">
|
1580 |
+
{feedback_data.get('feedback', '✨ เขียนได้ถูกต้องแล้วค่ะ!')}
|
1581 |
+
</p>
|
1582 |
+
</div>
|
1583 |
+
""", unsafe_allow_html=True)
|
1584 |
+
|
1585 |
+
def show_writing_tools():
|
1586 |
+
"""Display writing tools section"""
|
1587 |
+
with st.expander("✨ เครื่องมือช่วยเขียน | Writing Tools"):
|
1588 |
+
col1, col2 = st.columns(2)
|
1589 |
+
|
1590 |
+
with col1:
|
1591 |
+
if st.button("🎯 ขอคำใบ้", use_container_width=True):
|
1592 |
+
with st.spinner("กำลังสร้างคำใบ้..."):
|
1593 |
+
prompt = get_creative_prompt()
|
1594 |
+
st.markdown(f"""
|
1595 |
+
<div style="
|
1596 |
+
background-color: #f3e5f5;
|
1597 |
+
padding: 15px;
|
1598 |
+
border-radius: 8px;
|
1599 |
+
margin-top: 10px;
|
1600 |
+
">
|
1601 |
+
<div style="color: #6a1b9a;">💭 {prompt['thai']}</div>
|
1602 |
+
<div style="color: #666; font-style: italic;">
|
1603 |
+
💭 {prompt['eng']}
|
1604 |
+
</div>
|
1605 |
+
</div>
|
1606 |
+
""", unsafe_allow_html=True)
|
1607 |
+
|
1608 |
+
with col2:
|
1609 |
+
if st.button("📚 คำศัพท์แนะนำ", use_container_width=True):
|
1610 |
+
with st.spinner("กำลังค้นหาคำศัพท์..."):
|
1611 |
+
vocab_suggestions = get_vocabulary_suggestions()
|
1612 |
+
st.markdown("#### 📚 คำศัพท์น่ารู้")
|
1613 |
+
for word in vocab_suggestions:
|
1614 |
+
st.markdown(f"""
|
1615 |
+
<div style="
|
1616 |
+
background-color: #f5f5f5;
|
1617 |
+
padding: 10px;
|
1618 |
+
border-radius: 5px;
|
1619 |
+
margin: 5px 0;
|
1620 |
+
">
|
1621 |
+
• {word}
|
1622 |
+
</div>
|
1623 |
+
""", unsafe_allow_html=True)
|
1624 |
+
|
1625 |
+
def show_achievements():
|
1626 |
+
"""Display achievements and stats"""
|
1627 |
+
with st.container():
|
1628 |
+
# Points and Streak
|
1629 |
+
st.markdown(f"""
|
1630 |
+
<div style="
|
1631 |
+
background-color: #e3f2fd;
|
1632 |
+
padding: 20px;
|
1633 |
+
border-radius: 10px;
|
1634 |
+
margin-bottom: 20px;
|
1635 |
+
text-align: center;
|
1636 |
+
">
|
1637 |
+
<h3 style="color: #1e88e5; margin: 0;">
|
1638 |
+
🌟 คะแนนรวม: {st.session_state.points['total']}
|
1639 |
+
</h3>
|
1640 |
+
<p style="margin: 5px 0;">
|
1641 |
+
🔥 Streak ปัจจุบัน: {st.session_state.points['streak']} ประโยค
|
1642 |
+
</p>
|
1643 |
+
<p style="margin: 5px 0;">
|
1644 |
+
⭐ Streak สูงสุด: {st.session_state.points['max_streak']} ประโยค
|
1645 |
+
</p>
|
1646 |
+
</div>
|
1647 |
+
""", unsafe_allow_html=True)
|
1648 |
+
|
1649 |
+
# Writing Stats
|
1650 |
+
st.markdown("""
|
1651 |
+
<div style="margin-bottom: 15px;">
|
1652 |
+
<h3>📊 สถิติการเขียน</h3>
|
1653 |
+
</div>
|
1654 |
+
""", unsafe_allow_html=True)
|
1655 |
+
|
1656 |
+
col1, col2 = st.columns(2)
|
1657 |
+
with col1:
|
1658 |
+
st.metric(
|
1659 |
+
"ประโยคที่เขียนทั้งหมด",
|
1660 |
+
st.session_state.stats['total_sentences'],
|
1661 |
+
help="จำนวนประโยคทั้งหมดที่คุณได้เขียน"
|
1662 |
+
)
|
1663 |
+
st.metric(
|
1664 |
+
"ถูกต้องตั้งแต่แรก",
|
1665 |
+
st.session_state.stats['correct_first_try'],
|
1666 |
+
help="จำนวนประโยคที่ถูกต้องโดยไม่ต้องแก้ไข"
|
1667 |
+
)
|
1668 |
+
with col2:
|
1669 |
+
st.metric(
|
1670 |
+
"ความแม่นยำ",
|
1671 |
+
f"{st.session_state.stats['accuracy_rate']:.1f}%",
|
1672 |
+
help="เปอร์เซ็นต์ของประโยคที่ถูกต้องตั้งแต่แรก"
|
1673 |
+
)
|
1674 |
+
st.metric(
|
1675 |
+
"คำศัพท์ที่ใช้",
|
1676 |
+
len(st.session_state.stats['vocabulary_used']),
|
1677 |
+
help="จำนวนคำศัพท์ที่ไม่ซ้ำกันที่คุณได้ใช้"
|
1678 |
+
)
|
1679 |
+
|
1680 |
+
# Show Achievements
|
1681 |
+
st.markdown("""
|
1682 |
+
<div style="margin: 25px 0 15px 0;">
|
1683 |
+
<h3>🏆 ความสำเร็จ</h3>
|
1684 |
+
</div>
|
1685 |
+
""", unsafe_allow_html=True)
|
1686 |
+
|
1687 |
+
if st.session_state.achievements:
|
1688 |
+
for achievement in st.session_state.achievements:
|
1689 |
+
st.success(achievement)
|
1690 |
+
else:
|
1691 |
+
st.info("ยังไม่มีความสำเร็จ - เขียนต่อไปเพื่อปลดล็อกรางวัล!")
|
1692 |
+
|
1693 |
+
# Show available achievements
|
1694 |
+
st.markdown("""
|
1695 |
+
<div style="
|
1696 |
+
margin-top: 15px;
|
1697 |
+
padding: 15px;
|
1698 |
+
background-color: #f5f5f5;
|
1699 |
+
border-radius: 8px;
|
1700 |
+
">
|
1701 |
+
<p style="color: #666; margin-bottom: 10px;">
|
1702 |
+
รางวัลที่รอคุณอยู่:
|
1703 |
+
</p>
|
1704 |
+
<ul style="list-style-type: none; padding-left: 0;">
|
1705 |
+
<li>🌟 นักเขียนไร้ที่ติ: เขียนถูกต้อง 5 ประโยคติดต่อกัน</li>
|
1706 |
+
<li>📚 ราชาคำศัพท์: ใช้คำศัพท์ไม่ซ้ำกัน 50 คำ</li>
|
1707 |
+
<li>📖 นักแต่งนิทาน: เขียนเรื่องยาว 10 ประโยค</li>
|
1708 |
+
<li>👑 ราชาความแม่นยำ: มีอัตราความถูกต้อง 80% ขึ้นไป</li>
|
1709 |
+
</ul>
|
1710 |
+
</div>
|
1711 |
+
""", unsafe_allow_html=True)
|
1712 |
+
|
1713 |
+
def show_save_options():
|
1714 |
+
"""Display save and export options"""
|
1715 |
+
st.markdown("### 💾 บันทึกเรื่องราว")
|
1716 |
+
|
1717 |
+
if not st.session_state.story:
|
1718 |
+
st.info("เริ่มเขียนเรื่องราวก่อนเพื่อบันทึกผลงานของคุณ")
|
1719 |
+
return
|
1720 |
+
|
1721 |
+
col1, col2 = st.columns(2)
|
1722 |
+
with col1:
|
1723 |
+
# PDF Export
|
1724 |
+
if st.button("📑 บันทึกเป็น PDF", use_container_width=True):
|
1725 |
+
try:
|
1726 |
+
pdf = create_story_pdf()
|
1727 |
+
st.download_button(
|
1728 |
+
"📥 ดาวน์โหลด PDF",
|
1729 |
+
data=pdf,
|
1730 |
+
file_name=f"story_{datetime.now().strftime('%Y%m%d')}.pdf",
|
1731 |
+
mime="application/pdf"
|
1732 |
+
)
|
1733 |
+
except Exception as e:
|
1734 |
+
logging.error(f"Error creating PDF: {str(e)}")
|
1735 |
+
st.error("เกิดข้อผิดพลาดในการสร้างไฟล์ PDF กรุณาลองใหม่อีกครั้ง")
|
1736 |
+
|
1737 |
+
with col2:
|
1738 |
+
# JSON Save
|
1739 |
+
if st.button("💾 บันทึกความก้าวหน้า", use_container_width=True):
|
1740 |
+
try:
|
1741 |
+
story_data = {
|
1742 |
+
'timestamp': datetime.now().isoformat(),
|
1743 |
+
'level': st.session_state.level,
|
1744 |
+
'story': st.session_state.story,
|
1745 |
+
'achievements': st.session_state.achievements,
|
1746 |
+
'stats': convert_sets_to_lists(st.session_state.stats),
|
1747 |
+
'points': st.session_state.points
|
1748 |
+
}
|
1749 |
+
|
1750 |
+
st.download_button(
|
1751 |
+
"📥 ดาวน์โหลดไฟล์บันทึก",
|
1752 |
+
data=json.dumps(story_data, ensure_ascii=False, indent=2),
|
1753 |
+
file_name=f"story_progress_{datetime.now().strftime('%Y%m%d')}.json",
|
1754 |
+
mime="application/json"
|
1755 |
+
)
|
1756 |
+
except Exception as e:
|
1757 |
+
logging.error(f"Error saving progress: {str(e)}")
|
1758 |
+
st.error("เกิดข้อผิดพลาดในการบันทึกความก้าวหน้า กรุณาลองใหม่อีกครั้ง")
|
1759 |
+
|
1760 |
+
def show_sidebar():
|
1761 |
+
"""Display sidebar content"""
|
1762 |
+
st.sidebar.markdown("### ⚙️ การตั้งค่า")
|
1763 |
+
|
1764 |
+
# Progress Upload
|
1765 |
+
st.sidebar.markdown("#### 📂 โหลดความก้าวหน้า")
|
1766 |
+
uploaded_file = st.sidebar.file_uploader(
|
1767 |
+
"เลือกไฟล์ .json",
|
1768 |
+
type=['json'],
|
1769 |
+
help="เลือกไฟล์ความก้าวหน้าที่บันทึกไว้"
|
1770 |
+
)
|
1771 |
+
if uploaded_file:
|
1772 |
+
if st.sidebar.button("โหลดความก้าวหน้า", use_container_width=True):
|
1773 |
+
try:
|
1774 |
+
data = json.loads(uploaded_file.getvalue())
|
1775 |
+
load_progress(data)
|
1776 |
+
st.sidebar.success("โหลดความก้าวหน้าเรียบร้อย!")
|
1777 |
+
st.rerun()
|
1778 |
+
except Exception as e:
|
1779 |
+
logging.error(f"Error loading progress: {str(e)}")
|
1780 |
+
st.sidebar.error("เกิดข้อผิดพลาดในการโหลดไฟล์")
|
1781 |
+
|
1782 |
+
# Level Selection
|
1783 |
+
st.sidebar.markdown("#### 🎯 ระดับการเรียน")
|
1784 |
+
level = show_level_selection()
|
1785 |
+
st.session_state.level = level
|
1786 |
+
|
1787 |
+
# Theme Change Option
|
1788 |
+
if st.session_state.current_theme:
|
1789 |
+
st.sidebar.markdown("#### 🎨 ธีมเรื่องราว")
|
1790 |
+
if st.sidebar.button("🔄 เปลี่ยนธีม", use_container_width=True):
|
1791 |
+
st.session_state.current_theme = None
|
1792 |
+
st.session_state.theme_story_starter = None
|
1793 |
+
st.rerun()
|
1794 |
+
|
1795 |
+
# Reset Option
|
1796 |
+
st.sidebar.markdown("#### 🔄 รีเซ็ตเรื่องราว")
|
1797 |
+
if st.sidebar.button("เริ่มเรื่องใหม่", use_container_width=True):
|
1798 |
+
if st.session_state.story:
|
1799 |
+
if st.sidebar.checkbox("ยืนยันการเริ่มใหม่"):
|
1800 |
+
st.session_state.should_reset = True
|
1801 |
+
st.rerun()
|
1802 |
+
else:
|
1803 |
+
st.sidebar.info("ยังไม่มีเรื่องราวที่จะรีเซ็ต")
|
1804 |
+
|
1805 |
+
def show_story_input():
|
1806 |
+
"""Display story input section"""
|
1807 |
+
st.markdown("""
|
1808 |
+
<div class="thai-eng">
|
1809 |
+
<div class="thai">✏️ ถึงตาคุณแล้ว</div>
|
1810 |
+
<div class="eng">Your Turn</div>
|
1811 |
+
</div>
|
1812 |
+
""", unsafe_allow_html=True)
|
1813 |
+
|
1814 |
+
# Initialize clear_input flag if not exists
|
1815 |
+
if 'clear_input' not in st.session_state:
|
1816 |
+
st.session_state.clear_input = False
|
1817 |
+
|
1818 |
+
# Default value for text input
|
1819 |
+
default_value = "" if st.session_state.clear_input else st.session_state.get('text_input', "")
|
1820 |
+
|
1821 |
+
# If clear_input flag is True, reset it
|
1822 |
+
if st.session_state.clear_input:
|
1823 |
+
st.session_state.clear_input = False
|
1824 |
+
|
1825 |
+
# Input area
|
1826 |
+
text_input = st.text_area(
|
1827 |
+
"เขียนต่อจากเรื่องราว | Continue the story:",
|
1828 |
+
value=default_value,
|
1829 |
+
height=100,
|
1830 |
+
key="story_input_area",
|
1831 |
+
help="พิมพ์ประโยคภาษาอังกฤษเพื่อต่อเรื่อง",
|
1832 |
+
label_visibility="collapsed"
|
1833 |
+
)
|
1834 |
+
|
1835 |
+
# Save current input to session state
|
1836 |
+
st.session_state.text_input = text_input
|
1837 |
+
|
1838 |
+
# Submit button with character count
|
1839 |
+
col1, col2 = st.columns([3, 1])
|
1840 |
+
with col1:
|
1841 |
+
if st.button("📝 ส่งคำตอบ | Submit", use_container_width=True):
|
1842 |
+
if not text_input.strip():
|
1843 |
+
st.warning("กรุณาเขียนข้อความก่อนส่ง")
|
1844 |
+
return
|
1845 |
+
|
1846 |
+
try:
|
1847 |
+
with st.spinner("กำลังวิเคราะห์ประโยค..."):
|
1848 |
+
handle_story_submission(text_input.strip())
|
1849 |
+
except Exception as e:
|
1850 |
+
logging.error(f"Error submitting story: {str(e)}")
|
1851 |
+
st.error("เกิดข้อผิดพลาดในการส่งคำตอบ กรุณาลองใหม่อีกครั้ง")
|
1852 |
+
|
1853 |
+
with col2:
|
1854 |
+
char_count = len(text_input)
|
1855 |
+
st.markdown(f"""
|
1856 |
+
<div style="text-align: right; color: {'red' if char_count > 200 else '#666'};">
|
1857 |
+
{char_count}/200 ตัวอักษร
|
1858 |
+
</div>
|
1859 |
+
""", unsafe_allow_html=True)
|
1860 |
+
|
1861 |
+
def handle_story_submission(text: str):
|
1862 |
+
"""Handle story submission and processing"""
|
1863 |
+
if not st.session_state.story:
|
1864 |
+
st.error("กรุณาเลือกธีม���รื่องราวก่อนเริ่มเขียน")
|
1865 |
+
return
|
1866 |
+
|
1867 |
+
try:
|
1868 |
+
# Get feedback
|
1869 |
+
feedback_data = provide_feedback(text, st.session_state.level)
|
1870 |
+
st.session_state.feedback = feedback_data
|
1871 |
+
is_correct = not feedback_data.get('has_errors', False)
|
1872 |
+
|
1873 |
+
# Add user's sentence
|
1874 |
+
st.session_state.story.append({
|
1875 |
+
"role": "You",
|
1876 |
+
"content": text,
|
1877 |
+
"is_corrected": False,
|
1878 |
+
"is_correct": is_correct,
|
1879 |
+
"timestamp": datetime.now().isoformat()
|
1880 |
+
})
|
1881 |
+
|
1882 |
+
# Update vocabulary
|
1883 |
+
words = set(text.lower().split())
|
1884 |
+
st.session_state.stats['vocabulary_used'].update(words)
|
1885 |
+
|
1886 |
+
# Update points and achievements
|
1887 |
+
update_points(is_correct)
|
1888 |
+
update_achievements()
|
1889 |
+
|
1890 |
+
# Generate AI continuation
|
1891 |
+
try:
|
1892 |
+
logging.info("Attempting to generate AI continuation...")
|
1893 |
+
# ใช้ข้อความที่ถูกต้องสำหรับการต่อเรื่อง
|
1894 |
+
text_for_continuation = feedback_data['corrected'] if feedback_data.get('has_errors') else text
|
1895 |
+
|
1896 |
+
ai_response = generate_story_continuation(text_for_continuation, st.session_state.level)
|
1897 |
+
|
1898 |
+
# Log the AI response
|
1899 |
+
logging.info(f"AI Response generated: {ai_response}")
|
1900 |
+
|
1901 |
+
if ai_response and ai_response.strip():
|
1902 |
+
st.session_state.story.append({
|
1903 |
+
"role": "AI",
|
1904 |
+
"content": ai_response,
|
1905 |
+
"timestamp": datetime.now().isoformat()
|
1906 |
+
})
|
1907 |
+
logging.info("AI response added to story successfully")
|
1908 |
+
else:
|
1909 |
+
logging.error("AI generated empty response")
|
1910 |
+
# กรณีที่ AI ไม่สร้างประโยค ให้สร้างประโยคง่ายๆ ตาม theme
|
1911 |
+
fallback_response = generate_fallback_response(st.session_state.current_theme, st.session_state.level)
|
1912 |
+
st.session_state.story.append({
|
1913 |
+
"role": "AI",
|
1914 |
+
"content": fallback_response,
|
1915 |
+
"timestamp": datetime.now().isoformat()
|
1916 |
+
})
|
1917 |
+
except Exception as e:
|
1918 |
+
logging.error(f"Error generating AI continuation: {str(e)}")
|
1919 |
+
# สร้าง fallback response เมื่อเกิดข้อผิดพลาด
|
1920 |
+
fallback_response = generate_fallback_response(st.session_state.current_theme, st.session_state.level)
|
1921 |
+
st.session_state.story.append({
|
1922 |
+
"role": "AI",
|
1923 |
+
"content": fallback_response,
|
1924 |
+
"timestamp": datetime.now().isoformat()
|
1925 |
+
})
|
1926 |
+
|
1927 |
+
# Update session stats
|
1928 |
+
update_session_stats()
|
1929 |
+
|
1930 |
+
# Set flag to clear input on next rerun
|
1931 |
+
st.session_state.clear_input = True
|
1932 |
+
|
1933 |
+
# Rerun to update UI
|
1934 |
+
st.rerun()
|
1935 |
+
|
1936 |
+
except Exception as e:
|
1937 |
+
logging.error(f"Error in story submission: {str(e)}")
|
1938 |
+
raise
|
1939 |
+
|
1940 |
+
def generate_fallback_response(theme_id: str, level: str) -> str:
|
1941 |
+
"""Generate a simple fallback response when AI continuation fails"""
|
1942 |
+
try:
|
1943 |
+
theme = story_themes.get(theme_id, {})
|
1944 |
+
if theme:
|
1945 |
+
# ใช้คำศัพท์จาก theme และ level ที่เลือก
|
1946 |
+
vocab = theme.get('vocabulary', {}).get(level, [])
|
1947 |
+
if vocab:
|
1948 |
+
word = random.choice(vocab)
|
1949 |
+
|
1950 |
+
# สร้างประโยคตาม level
|
1951 |
+
if level == 'Beginner':
|
1952 |
+
return f"The story continues with {word}..."
|
1953 |
+
elif level == 'Intermediate':
|
1954 |
+
return f"Something interesting happened with the {word}."
|
1955 |
+
else: # Advanced
|
1956 |
+
return f"The mystery deepens as we discover more about the {word}."
|
1957 |
+
|
1958 |
+
# Default fallback if no theme-specific response can be generated
|
1959 |
+
return "The story continues..."
|
1960 |
+
|
1961 |
+
except Exception as e:
|
1962 |
+
logging.error(f"Error generating fallback response: {str(e)}")
|
1963 |
+
return "What happens next?"
|
1964 |
+
|
1965 |
+
def show_main_interface():
|
1966 |
+
"""Display main story interface"""
|
1967 |
+
col1, col2 = st.columns([3, 1])
|
1968 |
+
|
1969 |
+
with col1:
|
1970 |
+
# Story display
|
1971 |
+
st.markdown("""
|
1972 |
+
<div class="thai-eng">
|
1973 |
+
<div class="thai">📖 เรื่องราวของคุณ</div>
|
1974 |
+
<div class="eng">Your Story</div>
|
1975 |
+
</div>
|
1976 |
+
""", unsafe_allow_html=True)
|
1977 |
+
|
1978 |
+
show_story()
|
1979 |
+
|
1980 |
+
if st.session_state.story:
|
1981 |
+
show_story_input()
|
1982 |
+
|
1983 |
+
with col2:
|
1984 |
+
# Feedback section
|
1985 |
+
show_feedback_section()
|
1986 |
+
|
1987 |
+
# Writing tools
|
1988 |
+
show_writing_tools()
|
1989 |
+
|
1990 |
+
# Achievements
|
1991 |
+
with st.expander("🏆 ความสำเร็จ | Achievements"):
|
1992 |
+
show_achievements()
|
1993 |
+
|
1994 |
+
# Save options
|
1995 |
+
show_save_options()
|
1996 |
+
|
1997 |
+
# === 6. MAIN APPLICATION LOGIC ===
|
1998 |
+
def main():
|
1999 |
+
"""Main application entry point"""
|
2000 |
+
try:
|
2001 |
+
# Initialize states
|
2002 |
+
init_session_state()
|
2003 |
+
init_theme_state()
|
2004 |
+
|
2005 |
+
# Add watermark
|
2006 |
+
st.markdown("""
|
2007 |
+
<div style='position: fixed; bottom: 10px; right: 10px; z-index: 1000;
|
2008 |
+
opacity: 0.7; font-size: 0.8em; color: #666;'>
|
2009 |
+
Powered by JoyStory AI
|
2010 |
+
</div>
|
2011 |
+
""", unsafe_allow_html=True)
|
2012 |
+
|
2013 |
+
# Show header
|
2014 |
+
st.markdown("# 📖 JoyStory")
|
2015 |
+
show_welcome_section()
|
2016 |
+
show_parent_guide()
|
2017 |
+
|
2018 |
+
# Sidebar
|
2019 |
+
with st.sidebar:
|
2020 |
+
show_sidebar()
|
2021 |
+
|
2022 |
+
# Session Status Check
|
2023 |
+
check_session_status()
|
2024 |
+
|
2025 |
+
# Main content area
|
2026 |
+
main_container = st.container()
|
2027 |
+
with main_container:
|
2028 |
+
if not st.session_state.current_theme:
|
2029 |
+
show_theme_selection()
|
2030 |
+
else:
|
2031 |
+
show_main_interface()
|
2032 |
+
|
2033 |
+
# Handle reset if needed
|
2034 |
+
if st.session_state.should_reset:
|
2035 |
+
reset_story()
|
2036 |
+
|
2037 |
+
# Auto-save progress periodically
|
2038 |
+
if st.session_state.story:
|
2039 |
+
auto_save_progress()
|
2040 |
+
|
2041 |
+
except Exception as e:
|
2042 |
+
handle_application_error(e)
|
2043 |
+
|
2044 |
+
def check_session_status():
|
2045 |
+
"""Check and maintain session status"""
|
2046 |
+
try:
|
2047 |
+
# Update session duration
|
2048 |
+
if 'session_start' not in st.session_state:
|
2049 |
+
st.session_state.session_start = datetime.now()
|
2050 |
+
|
2051 |
+
# Check for session timeout (2 hours)
|
2052 |
+
session_duration = (datetime.now() - st.session_state.session_start).total_seconds()
|
2053 |
+
if session_duration > 7200: # 2 hours
|
2054 |
+
st.warning("เซสชันหมดอายุ กรุณาบันทึกความก้าวหน้าและรีเฟรชหน้าเว็บ")
|
2055 |
+
|
2056 |
+
# Check for inactivity (30 minutes)
|
2057 |
+
last_interaction = datetime.fromisoformat(st.session_state.last_interaction)
|
2058 |
+
inactivity_duration = (datetime.now() - last_interaction).total_seconds()
|
2059 |
+
if inactivity_duration > 1800: # 30 minutes
|
2060 |
+
st.info("ไม่มีกิจกรรมเป็นเวลานาน กรุณาบันทึกความก้าวหน้าเพื่อความปลอดภัย")
|
2061 |
+
|
2062 |
+
# Update stats if story exists
|
2063 |
+
if st.session_state.story:
|
2064 |
+
update_session_stats()
|
2065 |
+
|
2066 |
+
except Exception as e:
|
2067 |
+
logging.error(f"Error checking session status: {str(e)}")
|
2068 |
+
|
2069 |
+
def auto_save_progress():
|
2070 |
+
"""Automatically save progress to session state"""
|
2071 |
+
try:
|
2072 |
+
current_progress = {
|
2073 |
+
'timestamp': datetime.now().isoformat(),
|
2074 |
+
'story': st.session_state.story,
|
2075 |
+
'stats': st.session_state.stats,
|
2076 |
+
'points': st.session_state.points,
|
2077 |
+
'achievements': st.session_state.achievements
|
2078 |
+
}
|
2079 |
+
|
2080 |
+
# Save to session state
|
2081 |
+
if 'auto_save' not in st.session_state:
|
2082 |
+
st.session_state.auto_save = {}
|
2083 |
+
|
2084 |
+
st.session_state.auto_save = current_progress
|
2085 |
+
|
2086 |
+
# Add timestamp for last auto-save
|
2087 |
+
st.session_state.last_auto_save = datetime.now().isoformat()
|
2088 |
+
|
2089 |
+
except Exception as e:
|
2090 |
+
logging.error(f"Error in auto-save: {str(e)}")
|
2091 |
+
|
2092 |
+
def handle_application_error(error: Exception):
|
2093 |
+
"""Handle application-wide errors"""
|
2094 |
+
logging.error(f"Application error: {str(error)}")
|
2095 |
+
|
2096 |
+
error_message = """
|
2097 |
+
<div style="
|
2098 |
+
background-color: #ffebee;
|
2099 |
+
padding: 20px;
|
2100 |
+
border-radius: 10px;
|
2101 |
+
border-left: 4px solid #c62828;
|
2102 |
+
margin: 20px 0;
|
2103 |
+
">
|
2104 |
+
<h3 style="color: #c62828; margin: 0 0 10px 0;">
|
2105 |
+
⚠️ เกิดข้อผิดพลาดในระบบ
|
2106 |
+
</h3>
|
2107 |
+
<p style="color: #333; margin: 0;">
|
2108 |
+
กรุณาลองใหม่อีกครั้ง หรือติดต่อผู้ดูแลระบบ
|
2109 |
+
</p>
|
2110 |
+
</div>
|
2111 |
+
"""
|
2112 |
+
|
2113 |
+
st.markdown(error_message, unsafe_allow_html=True)
|
2114 |
+
|
2115 |
+
# Show technical details in expander
|
2116 |
+
with st.expander("รายละเอียดข้อผิดพลาด (สำหรับผู้ดูแลระบบ)"):
|
2117 |
+
st.code(f"""
|
2118 |
+
Error Type: {type(error).__name__}
|
2119 |
+
Error Message: {str(error)}
|
2120 |
+
Timestamp: {datetime.now().isoformat()}
|
2121 |
+
""")
|
2122 |
+
|
2123 |
+
def show_debug_info():
|
2124 |
+
"""Show debug information (development only)"""
|
2125 |
+
if st.session_state.get('debug_mode'):
|
2126 |
+
with st.expander("🔧 Debug Information"):
|
2127 |
+
st.json({
|
2128 |
+
'session_state': {
|
2129 |
+
key: str(value) if isinstance(value, (set, datetime)) else value
|
2130 |
+
for key, value in st.session_state.items()
|
2131 |
+
if key not in ['client', '_client']
|
2132 |
+
}
|
2133 |
+
})
|
2134 |
+
|
2135 |
+
# Add CSS for loading animation
|
2136 |
+
st.markdown("""
|
2137 |
+
<style>
|
2138 |
+
@keyframes pulse {
|
2139 |
+
0% { opacity: 0.6; }
|
2140 |
+
50% { opacity: 1; }
|
2141 |
+
100% { opacity: 0.6; }
|
2142 |
+
}
|
2143 |
+
|
2144 |
+
.loading-animation {
|
2145 |
+
animation: pulse 1.5s infinite;
|
2146 |
+
background-color: #f0f2f5;
|
2147 |
+
border-radius: 8px;
|
2148 |
+
padding: 20px;
|
2149 |
+
text-align: center;
|
2150 |
+
margin: 20px 0;
|
2151 |
+
}
|
2152 |
+
|
2153 |
+
/* Custom Scrollbar */
|
2154 |
+
::-webkit-scrollbar {
|
2155 |
+
width: 8px;
|
2156 |
+
height: 8px;
|
2157 |
+
}
|
2158 |
+
|
2159 |
+
::-webkit-scrollbar-track {
|
2160 |
+
background: #f1f1f1;
|
2161 |
+
border-radius: 4px;
|
2162 |
+
}
|
2163 |
+
|
2164 |
+
::-webkit-scrollbar-thumb {
|
2165 |
+
background: #888;
|
2166 |
+
border-radius: 4px;
|
2167 |
+
}
|
2168 |
+
|
2169 |
+
::-webkit-scrollbar-thumb:hover {
|
2170 |
+
background: #666;
|
2171 |
+
}
|
2172 |
+
|
2173 |
+
/* Toast Notifications */
|
2174 |
+
.toast-notification {
|
2175 |
+
position: fixed;
|
2176 |
+
bottom: 20px;
|
2177 |
+
right: 20px;
|
2178 |
+
padding: 15px 25px;
|
2179 |
+
background-color: #333;
|
2180 |
+
color: white;
|
2181 |
+
border-radius: 8px;
|
2182 |
+
z-index: 1000;
|
2183 |
+
animation: slideIn 0.3s ease-out;
|
2184 |
+
}
|
2185 |
+
|
2186 |
+
@keyframes slideIn {
|
2187 |
+
from { transform: translateX(100%); }
|
2188 |
+
to { transform: translateX(0); }
|
2189 |
+
}
|
2190 |
+
|
2191 |
+
/* Progress Indicator */
|
2192 |
+
.progress-bar {
|
2193 |
+
width: 100%;
|
2194 |
+
height: 4px;
|
2195 |
+
background-color: #e0e0e0;
|
2196 |
+
border-radius: 2px;
|
2197 |
+
overflow: hidden;
|
2198 |
+
}
|
2199 |
+
|
2200 |
+
.progress-bar-fill {
|
2201 |
+
height: 100%;
|
2202 |
+
background-color: #1e88e5;
|
2203 |
+
transition: width 0.3s ease;
|
2204 |
+
}
|
2205 |
+
</style>
|
2206 |
+
""", unsafe_allow_html=True)
|
2207 |
+
|
2208 |
+
if __name__ == "__main__":
|
2209 |
+
try:
|
2210 |
+
main()
|
2211 |
+
except Exception as e:
|
2212 |
+
handle_application_error(e)
|