|
import streamlit as st |
|
import json |
|
import datetime |
|
from openai import OpenAI |
|
from typing import Dict, List, Set |
|
import io |
|
from reportlab.lib import colors |
|
from reportlab.lib.pagesizes import A4 |
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
|
from reportlab.lib.units import inch |
|
from reportlab.pdfgen import canvas |
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image |
|
from reportlab.pdfbase import pdfmetrics |
|
from reportlab.pdfbase.ttfonts import TTFont |
|
from datetime import datetime |
|
|
|
|
|
achievements_list = { |
|
'perfect_writer': { |
|
'name': '🌟 นักเขียนไร้ที่ติ', |
|
'description': 'เขียนถูกต้อง 5 ประโยคติดต่อกัน', |
|
'condition': lambda: st.session_state.points['streak'] >= 5 |
|
}, |
|
'vocabulary_master': { |
|
'name': '📚 ราชาคำศัพท์', |
|
'description': 'ใช้คำศัพท์ไม่ซ้ำกัน 50 คำ', |
|
'condition': lambda: len(st.session_state.stats['vocabulary_used']) >= 50 |
|
}, |
|
'quick_learner': { |
|
'name': '🚀 นักเรียนจอมขยัน', |
|
'description': 'แก้ไขประโยคให้ถูกต้องภายใน 3 วินาที', |
|
'condition': lambda: True |
|
}, |
|
'story_master': { |
|
'name': '📖 นักแต่งนิทาน', |
|
'description': 'เขียนเรื่องยาว 10 ประโยค', |
|
'condition': lambda: len(st.session_state.story) >= 10 |
|
}, |
|
'accuracy_king': { |
|
'name': '👑 ราชาความแม่นยำ', |
|
'description': 'มีอัตราความถูกต้อง 80% ขึ้นไป (อย่างน้อย 10 ประโยค)', |
|
'condition': lambda: ( |
|
st.session_state.stats['total_sentences'] >= 10 and |
|
st.session_state.stats['accuracy_rate'] >= 80 |
|
) |
|
} |
|
} |
|
|
|
|
|
st.set_page_config( |
|
page_title="JoyStory - Interactive Story Adventure", |
|
page_icon="📖", |
|
layout="wide", |
|
initial_sidebar_state="collapsed", |
|
) |
|
|
|
|
|
client = OpenAI() |
|
|
|
|
|
level_options = { |
|
'Beginner': { |
|
'thai_name': 'ระดับเริ่มต้น (ป.1-3)', |
|
'age_range': '7-9 ปี', |
|
'description': 'เหมาะสำหรับน้องๆ ที่เริ่มเรียนรู้การเขียนประโยคภาษาอังกฤษ', |
|
'features': [ |
|
'ประโยคสั้นๆ ง่ายๆ', |
|
'คำศัพท์พื้นฐานที่ใช้ในชีวิตประจำวัน', |
|
'มีคำแนะนำภาษาไทยละเอียด', |
|
'เน้นการใช้ Present Simple Tense' |
|
] |
|
}, |
|
'Intermediate': { |
|
'thai_name': 'ระดับกลาง (ป.4-6)', |
|
'age_range': '10-12 ปี', |
|
'description': 'เหมาะสำหรับน้องๆ ที่สามารถเขียนประโยคพื้นฐานได้แล้ว', |
|
'features': [ |
|
'ประโยคซับซ้อนขึ้น', |
|
'เริ่มใช้ Past Tense ได้', |
|
'คำศัพท์หลากหลายขึ้น', |
|
'สามารถเขียนเรื่องราวต่อเนื่องได้' |
|
] |
|
}, |
|
'Advanced': { |
|
'thai_name': 'ระดับก้าวหน้า (ม.1-3)', |
|
'age_range': '13-15 ปี', |
|
'description': 'เหมาะสำหรับน้องๆ ที่มีพื้นฐานภาษาอังกฤษดี', |
|
'features': [ |
|
'เขียนเรื่องราวได้หลากหลายรูปแบบ', |
|
'ใช้ Tense ต่างๆ ได้', |
|
'คำศัพท์ระดับสูงขึ้น', |
|
'สามารถแต่งเรื่องที่ซับซ้อนได้' |
|
] |
|
} |
|
} |
|
|
|
|
|
st.markdown(""" |
|
<style> |
|
@import url('https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap'); |
|
|
|
.thai-eng { |
|
font-size: 1.1em; |
|
padding: 10px; |
|
background-color: #f8f9fa; |
|
border-radius: 8px; |
|
margin: 10px 0; |
|
} |
|
.thai { |
|
color: #1e88e5; |
|
font-family: 'Sarabun', sans-serif; |
|
} |
|
.eng { |
|
color: #333; |
|
} |
|
.level-info { |
|
background-color: #e3f2fd; |
|
padding: 10px; |
|
border-radius: 8px; |
|
margin: 10px 0; |
|
font-size: 0.9em; |
|
} |
|
.parent-guide { |
|
background-color: #fff3e0; |
|
padding: 15px; |
|
border-radius: 8px; |
|
margin: 15px 0; |
|
border-left: 4px solid #ff9800; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
def init_session_state(): |
|
if 'story' not in st.session_state: |
|
st.session_state.story = [] |
|
if 'feedback' not in st.session_state: |
|
st.session_state.feedback = None |
|
if 'level' not in st.session_state: |
|
st.session_state.level = 'Beginner' |
|
if 'unique_words' not in st.session_state: |
|
st.session_state.unique_words = set() |
|
if 'total_words' not in st.session_state: |
|
st.session_state.total_words = 0 |
|
if 'should_reset' not in st.session_state: |
|
st.session_state.should_reset = False |
|
if 'user_input' not in st.session_state: |
|
st.session_state.user_input = "" |
|
if 'points' not in st.session_state: |
|
st.session_state.points = { |
|
'total': 0, |
|
'perfect_sentences': 0, |
|
'corrections_made': 0, |
|
'streak': 0, |
|
'max_streak': 0 |
|
} |
|
if 'stats' not in st.session_state: |
|
st.session_state.stats = { |
|
'total_sentences': 0, |
|
'correct_first_try': 0, |
|
'accuracy_rate': 0.0, |
|
'vocabulary_used': set() |
|
} |
|
if 'achievements' not in st.session_state: |
|
st.session_state.achievements = [] |
|
|
|
init_session_state() |
|
|
|
|
|
def clear_input(): |
|
st.session_state.user_input = "" |
|
|
|
|
|
def submit_story(): |
|
if st.session_state.text_input.strip(): |
|
user_text = st.session_state.text_input |
|
|
|
|
|
words = set(user_text.lower().split()) |
|
st.session_state.stats['vocabulary_used'].update(words) |
|
|
|
|
|
feedback_data = provide_feedback(user_text, st.session_state.level) |
|
is_correct = not feedback_data.get('has_errors', False) |
|
|
|
|
|
update_points(is_correct) |
|
|
|
|
|
update_achievements() |
|
|
|
|
|
story_index = len(st.session_state.story) |
|
st.session_state.story.append({ |
|
"role": "You", |
|
"content": user_text, |
|
"is_corrected": False |
|
}) |
|
|
|
try: |
|
|
|
feedback_data = provide_feedback(user_text, st.session_state.level) |
|
st.session_state.feedback = feedback_data |
|
|
|
|
|
if feedback_data['has_errors']: |
|
st.markdown(f""" |
|
<div style='background-color: #fff3e0; padding: 10px; border-radius: 5px; margin: 10px 0;'> |
|
<p style='color: #ff6d00;'>🎯 คำแนะนำ:</p> |
|
<p>{feedback_data['feedback']}</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
ai_response = generate_story_continuation( |
|
feedback_data['corrected'] if feedback_data['has_errors'] else user_text, |
|
st.session_state.level |
|
) |
|
st.session_state.story.append({"role": "AI", "content": ai_response}) |
|
|
|
except Exception as e: |
|
st.error("เกิดข้อผิดพลาดในการวิเคราะห์ประโยค กรุณาลองใหม่อีกครั้ง") |
|
|
|
st.error(f"Debug - Error in submit_story: {str(e)}") |
|
|
|
|
|
st.session_state.text_input = "" |
|
|
|
def show_welcome_section(): |
|
st.markdown(""" |
|
<div class="welcome-header"> |
|
<div class="thai"> |
|
🌟 ยินดีต้อนรับสู่ JoyStory - แอพฝึกเขียนภาษาอังกฤษแสนสนุก! |
|
<br> |
|
เรียนรู้ภาษาอังกฤษผ่านการเขียนเรื่องราวด้วยตัวเอง พร้อมผู้ช่วย AI ที่จะช่วยแนะนำและให้กำลังใจ |
|
</div> |
|
<div class="eng"> |
|
Welcome to JoyStory - Fun English Writing Adventure! |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
def show_parent_guide(): |
|
with st.expander("📖 คำแนะนำสำหรับผู้ปกครอง | Parent's Guide"): |
|
st.markdown(""" |
|
<div class="parent-guide"> |
|
<h4>คำแนะนำในการใช้งาน</h4> |
|
<ul> |
|
<li>แนะนำให้นั่งเขียนเรื่องราวร่วมกับน้องๆ</li> |
|
<li>ช่วยอธิบายคำแนะนำและคำศัพท์ที่น้องๆ ไม่เข้าใจ</li> |
|
<li>ให้กำลังใจและชื่นชมเมื่อน้องๆ เขียนได้ดี</li> |
|
<li>ใช้เวลาในการเขียนแต่ละครั้งไม่เกิน 20-30 นาที</li> |
|
</ul> |
|
<p>💡 เคล็ดลับ: ให้น้องๆ พูดเรื่องราวที่อยากเขียนเป็นภาษาไทยก่อน |
|
แล้วค่อยๆ ช่วยกันแปลงเป็นภาษาอังกฤษ</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
def generate_story_continuation(user_input: str, level: str) -> str: |
|
"""Generate AI story continuation using ChatGPT with level-appropriate content.""" |
|
level_context = { |
|
'Beginner': """ |
|
Role: You are a teaching assistant for Thai students in grades 1-3. |
|
Rules: |
|
- Use only 1-2 VERY simple sentences |
|
- Use Present Simple Tense only |
|
- Use basic vocabulary (family, school, daily activities) |
|
- Each sentence should be 5-7 words maximum |
|
- Focus on clear, basic responses |
|
Example responses: |
|
- "The cat sits under the tree." |
|
- "The boy plays with his toy car." |
|
- "They walk to school together." |
|
""", |
|
'Intermediate': """ |
|
Role: You are a teaching assistant for Thai students in grades 4-6. |
|
Rules: |
|
- Use exactly 2 sentences maximum |
|
- Can use Present or Past Tense |
|
- Keep each sentence under 12 words |
|
- Use grade-appropriate vocabulary |
|
- Add simple descriptions but stay concise |
|
Example responses: |
|
- "The brown cat jumped over the tall wooden fence. It landed softly in the garden." |
|
- "Tom carefully opened his mysterious new book. The colorful pages showed amazing magical creatures." |
|
""", |
|
'Advanced': """ |
|
Role: You are a teaching assistant for Thai students in grades 7-9. |
|
Rules: |
|
- Use 2-3 sentences maximum (no more!) |
|
- Various tenses are allowed |
|
- No strict word limit per sentence, but keep overall response concise |
|
- Use more sophisticated vocabulary and sentence structures |
|
- Create engaging responses that encourage creative continuation |
|
- Focus on quality and natural flow rather than sentence length |
|
Example responses: |
|
- "Sarah discovered an ancient-looking letter hidden beneath the creaky floorboards. The yellowed paper contained a mysterious message." |
|
- "As the storm clouds gathered overhead, James remembered the old legend about the mountain. Lightning illuminated the winding path that led to the cave entrance." |
|
""" |
|
} |
|
|
|
try: |
|
story_context = '\n'.join([entry['content'] for entry in st.session_state.story[-3:]]) |
|
|
|
response = client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": f"""You are a storytelling assistant for Thai students. |
|
{level_context[level]} |
|
CRUCIAL GUIDELINES: |
|
- NEVER exceed the maximum number of sentences for the level |
|
- Create openings for student's creativity |
|
- Do not resolve plot points or conclude the story |
|
- Avoid using 'suddenly' or 'then' |
|
- Make each sentence meaningful but incomplete |
|
- Leave room for the student to develop the story |
|
|
|
Remember: This is interactive storytelling - let the student drive the story forward."""}, |
|
{"role": "user", "content": f"Story context (recent):\n{story_context}\nStudent's input:\n{user_input}\nProvide a brief continuation:"} |
|
], |
|
max_tokens={ |
|
'Beginner': 30, |
|
'Intermediate': 40, |
|
'Advanced': 50 |
|
}[level], |
|
temperature=0.7, |
|
presence_penalty=0.6, |
|
frequency_penalty=0.6 |
|
) |
|
|
|
|
|
response_text = response.choices[0].message.content.strip() |
|
sentences = [s.strip() for s in response_text.split('.') if s.strip()] |
|
|
|
|
|
max_sentences = {'Beginner': 2, 'Intermediate': 2, 'Advanced': 3} |
|
if len(sentences) > max_sentences[level]: |
|
sentences = sentences[:max_sentences[level]] |
|
|
|
|
|
response_text = '. '.join(sentences) + '.' |
|
|
|
return response_text |
|
|
|
except Exception as e: |
|
st.error(f"Error generating story continuation: {str(e)}") |
|
return "I'm having trouble continuing the story. Please try again." |
|
|
|
|
|
def apply_correction(story_index: int, corrected_text: str): |
|
"""Apply correction to a specific story entry.""" |
|
if 0 <= story_index < len(st.session_state.story): |
|
original_text = st.session_state.story[story_index]['content'] |
|
|
|
if 'corrections' not in st.session_state: |
|
st.session_state.corrections = {} |
|
|
|
st.session_state.corrections[story_index] = { |
|
'original': original_text, |
|
'corrected': corrected_text, |
|
'timestamp': datetime.datetime.now().isoformat() |
|
} |
|
|
|
st.session_state.story[story_index]['content'] = corrected_text |
|
st.session_state.story[story_index]['is_corrected'] = True |
|
|
|
|
|
st.success("✅ แก้ไขประโยคเรียบร้อยแล้ว!") |
|
|
|
def get_vocabulary_suggestions() -> List[str]: |
|
"""Get contextual vocabulary suggestions with Thai translations.""" |
|
try: |
|
recent_story = '\n'.join([entry['content'] for entry in st.session_state.story[-3:]] if st.session_state.story else "Story just started") |
|
|
|
response = client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": f"""You are a Thai-English bilingual teacher. |
|
Suggest 5 English words with their Thai translations and examples. |
|
Format each suggestion as: |
|
word (type) - คำแปล | example sentence |
|
Make sure words match {st.session_state.level} level."""}, |
|
{"role": "user", "content": f"Story context:\n{recent_story}\n\nSuggest 5 relevant words with Thai translations:"} |
|
], |
|
max_tokens=200, |
|
temperature=0.8 |
|
) |
|
return response.choices[0].message.content.split('\n') |
|
except Exception as e: |
|
st.error(f"Error getting vocabulary suggestions: {str(e)}") |
|
return ["happy (adj) - มีความสุข | I am happy today", |
|
"run (verb) - วิ่ง | The dog runs fast", |
|
"tree (noun) - ต้นไม้ | A tall tree"] |
|
|
|
|
|
if st.button("Get Vocabulary Ideas"): |
|
vocab_suggestions = get_vocabulary_suggestions() |
|
st.markdown("#### 📚 Suggested Words") |
|
for word in vocab_suggestions: |
|
st.markdown(f"• *{word}*") |
|
|
|
|
|
if st.session_state.feedback: |
|
st.markdown(""" |
|
<div style='background-color: #f0f2f6; padding: 8px; border-radius: 4px; margin-bottom: 10px;'> |
|
📝 <i>{}</i> |
|
</div> |
|
""".format(st.session_state.feedback), unsafe_allow_html=True) |
|
|
|
|
|
def get_creative_prompt() -> Dict[str, str]: |
|
"""Generate a short, simple bilingual creative prompt.""" |
|
try: |
|
response = client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": """Create very short story prompts in both English and Thai. |
|
Keep it simple and under 6 words each. |
|
Example formats: |
|
- "What did the cat find?" |
|
- "Where did they go next?" |
|
- "How does the story end?" |
|
"""}, |
|
{"role": "user", "content": "Generate a simple, short story prompt:"} |
|
], |
|
max_tokens=50, |
|
temperature=0.7 |
|
) |
|
prompt_eng = response.choices[0].message.content |
|
|
|
|
|
response_thai = client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": "Translate to short Thai prompt, keep it simple and natural:"}, |
|
{"role": "user", "content": prompt_eng} |
|
], |
|
max_tokens=50, |
|
temperature=0.7 |
|
) |
|
prompt_thai = response_thai.choices[0].message.content |
|
|
|
return {"eng": prompt_eng, "thai": prompt_thai} |
|
except Exception as e: |
|
st.error(f"Error generating creative prompt: {str(e)}") |
|
return { |
|
"eng": "What happens next?", |
|
"thai": "แล้วอะไรจะเกิดขึ้นต่อ?" |
|
} |
|
|
|
def provide_feedback(text: str, level: str) -> Dict[str, str]: |
|
"""Provide feedback and corrected sentence.""" |
|
try: |
|
response = client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": f"""You are a Thai English teacher helping {level} students. |
|
Your task is to review the student's sentence and provide feedback. |
|
|
|
Return your response in this EXACT format only (must be valid JSON): |
|
{{ |
|
"feedback": "(ข้อเสนอแนะภาษาไทย)", |
|
"corrected": "(ประโยคภาษาอังกฤษที่ถูกต้อง)", |
|
"has_errors": true/false |
|
}} |
|
|
|
Example 1 - with error: |
|
{{ |
|
"feedback": "คำว่า 'go' เมื่อพูดถึงเหตุการณ์ในอดีต ต้องเปลี่ยนเป็น 'went'", |
|
"corrected": "I went to school yesterday.", |
|
"has_errors": true |
|
}} |
|
|
|
Example 2 - no error: |
|
{{ |
|
"feedback": "เขียนได้ถูกต้องแล้วค่ะ ประโยคสื่อความหมายได้ดี", |
|
"corrected": "The cat is sleeping.", |
|
"has_errors": false |
|
}}"""}, |
|
{"role": "user", "content": f"Review this sentence and provide feedback in the specified JSON format: {text}"} |
|
], |
|
max_tokens=200, |
|
temperature=0.3 |
|
) |
|
|
|
|
|
response_text = response.choices[0].message.content.strip() |
|
|
|
try: |
|
|
|
st.write("Debug - Response received:", response_text) |
|
|
|
|
|
feedback_data = json.loads(response_text) |
|
|
|
|
|
if all(key in feedback_data for key in ['feedback', 'corrected', 'has_errors']): |
|
return feedback_data |
|
else: |
|
raise ValueError("Missing required fields in response") |
|
|
|
except json.JSONDecodeError as json_err: |
|
st.error(f"Debug - JSON Error: {str(json_err)}") |
|
raise |
|
|
|
except Exception as e: |
|
st.error(f"Debug - Error: {str(e)}") |
|
return { |
|
"feedback": "⚠️ ระบบไม่สามารถวิเคราะห์ประโยคได้ กรุณาลองใหม่อีกครั้ง", |
|
"corrected": text, |
|
"has_errors": False |
|
} |
|
|
|
def update_points(is_correct_first_try: bool): |
|
"""อัพเดตคะแนนตามผลการเขียน""" |
|
base_points = 10 |
|
if is_correct_first_try: |
|
points = base_points * 2 |
|
st.session_state.points['perfect_sentences'] += 1 |
|
st.session_state.points['streak'] += 1 |
|
if st.session_state.points['streak'] > st.session_state.points['max_streak']: |
|
st.session_state.points['max_streak'] = st.session_state.points['streak'] |
|
else: |
|
points = base_points // 2 |
|
st.session_state.points['corrections_made'] += 1 |
|
st.session_state.points['streak'] = 0 |
|
|
|
st.session_state.points['total'] += points |
|
|
|
st.session_state.stats['total_sentences'] += 1 |
|
if is_correct_first_try: |
|
st.session_state.stats['correct_first_try'] += 1 |
|
st.session_state.stats['accuracy_rate'] = ( |
|
st.session_state.stats['correct_first_try'] / |
|
st.session_state.stats['total_sentences'] * 100 |
|
) |
|
|
|
|
|
def show_achievements(): |
|
"""แสดงความสำเร็จและสถิติ""" |
|
with st.container(): |
|
|
|
st.markdown(f""" |
|
<div style='background-color: #f0f8ff; padding: 15px; border-radius: 10px; margin-bottom: 15px;'> |
|
<h3 style='color: #1e88e5; margin: 0;'>🌟 คะแนนรวม: {st.session_state.points['total']}</h3> |
|
<p style='margin: 5px 0;'>Streak ปัจจุบัน: {st.session_state.points['streak']} ประโยค</p> |
|
<p style='margin: 5px 0;'>Streak สูงสุด: {st.session_state.points['max_streak']} ประโยค</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown(""" |
|
<div style='margin-bottom: 10px;'> |
|
<h3>📊 สถิติการเขียน</h3> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
st.metric( |
|
"ประโยคที่เขียนทั้งหมด", |
|
st.session_state.stats['total_sentences'], |
|
help="จำนวนประโยคทั้งหมดที่คุณได้เขียน" |
|
) |
|
st.metric( |
|
"ถูกต้องตั้งแต่แรก", |
|
st.session_state.stats['correct_first_try'], |
|
help="จำนวนประโยคที่ถูกต้องโดยไม่ต้องแก้ไข" |
|
) |
|
with col2: |
|
st.metric( |
|
"ความแม่นยำ", |
|
f"{st.session_state.stats['accuracy_rate']:.1f}%", |
|
help="เปอร์เซ็นต์ของประโยคที่ถูกต้องตั้งแต่แรก" |
|
) |
|
st.metric( |
|
"คำศัพท์ที่ใช้", |
|
len(st.session_state.stats['vocabulary_used']), |
|
help="จำนวนคำศัพท์ที่ไม่ซ้ำกันที่คุณได้ใช้" |
|
) |
|
|
|
|
|
st.markdown(""" |
|
<div style='margin: 20px 0 10px 0;'> |
|
<h3>🏆 ความสำเร็จ</h3> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
if st.session_state.achievements: |
|
for achievement in st.session_state.achievements: |
|
st.success(achievement) |
|
else: |
|
st.info("ยังไม่มีความสำเร็จ - เขียนต่อไปเพื่อปลดล็อกรางวัล!") |
|
|
|
|
|
if not st.session_state.achievements: |
|
st.markdown(""" |
|
<div style='margin-top: 15px; font-size: 0.9em; color: #666;'> |
|
เป้าหมายที่จะได้รับความสำเร็จ: |
|
</div> |
|
""", unsafe_allow_html=True) |
|
st.markdown(""" |
|
- 🌟 นักเขียนไร้ที่ติ: เขียนถูกต้อง 5 ประโยคติดต่อกัน |
|
- 📚 ราชาคำศัพท์: ใช้คำศัพท์ไม่ซ้ำกัน 50 คำ |
|
- 📖 นักแต่งนิทาน: เขียนเรื่องยาว 10 ประโยค |
|
- 👑 ราชาความแม่นยำ: มีอัตราความถูกต้อง 80% ขึ้นไป |
|
""") |
|
|
|
def update_achievements(): |
|
"""ตรวจสอบและอัพเดตความสำเร็จ""" |
|
current_achievements = st.session_state.achievements |
|
|
|
|
|
if st.session_state.points['streak'] >= 5 and "🌟 นักเขียนไร้ที่ติ" not in current_achievements: |
|
current_achievements.append("🌟 นักเขียนไร้ที่ติ") |
|
st.success("🎉 ได้รับความสำเร็จใหม่: นักเขียนไร้ที่ติ!") |
|
|
|
if len(st.session_state.stats['vocabulary_used']) >= 50 and "📚 ราชาคำศัพท์" not in current_achievements: |
|
current_achievements.append("📚 ราชาคำศัพท์") |
|
st.success("🎉 ได้รับความสำเร็จใหม่: ราชาคำศัพท์!") |
|
|
|
if len(st.session_state.story) >= 10 and "📖 นักแต่งนิทาน" not in current_achievements: |
|
current_achievements.append("📖 นักแต่งนิทาน") |
|
st.success("🎉 ได้รับความสำเร็จใหม่: นักแต่งนิทาน!") |
|
|
|
if (st.session_state.stats['total_sentences'] >= 10 and |
|
st.session_state.stats['accuracy_rate'] >= 80 and |
|
"👑 ราชาความแม่นยำ" not in current_achievements): |
|
current_achievements.append("👑 ราชาความแม่นยำ") |
|
st.success("🎉 ได้รับความสำเร็จใหม่: ราชาความแม่นยำ!") |
|
|
|
def create_story_pdf(): |
|
"""สร้าง PDF จากเรื่องราวที่เขียน""" |
|
buffer = io.BytesIO() |
|
doc = SimpleDocTemplate( |
|
buffer, |
|
pagesize=A4, |
|
rightMargin=72, |
|
leftMargin=72, |
|
topMargin=72, |
|
bottomMargin=72 |
|
) |
|
|
|
|
|
styles = getSampleStyleSheet() |
|
title_style = ParagraphStyle( |
|
'CustomTitle', |
|
parent=styles['Heading1'], |
|
fontSize=24, |
|
spaceAfter=30, |
|
alignment=1 |
|
) |
|
|
|
story_style = ParagraphStyle( |
|
'StoryText', |
|
parent=styles['Normal'], |
|
fontSize=12, |
|
spaceBefore=12, |
|
spaceAfter=12, |
|
leading=16 |
|
) |
|
|
|
stats_style = ParagraphStyle( |
|
'Stats', |
|
parent=styles['Normal'], |
|
fontSize=10, |
|
textColor=colors.gray, |
|
alignment=1 |
|
) |
|
|
|
|
|
elements = [] |
|
|
|
|
|
elements.append(Paragraph("My English Story Adventure", title_style)) |
|
elements.append(Paragraph( |
|
f"Created by: {st.session_state.get('player_name', 'Young Writer')}<br/>" |
|
f"Date: {datetime.now().strftime('%B %d, %Y')}<br/>" |
|
f"Level: {st.session_state.level}", |
|
stats_style |
|
)) |
|
elements.append(Spacer(1, 30)) |
|
|
|
|
|
stats_text = ( |
|
f"Total Points: {st.session_state.points['total']}<br/>" |
|
f"Perfect Sentences: {st.session_state.points['perfect_sentences']}<br/>" |
|
f"Accuracy Rate: {st.session_state.stats['accuracy_rate']:.1f}%<br/>" |
|
f"Unique Words Used: {len(st.session_state.stats['vocabulary_used'])}" |
|
) |
|
elements.append(Paragraph(stats_text, stats_style)) |
|
elements.append(Spacer(1, 30)) |
|
|
|
|
|
elements.append(Paragraph("The Story", styles['Heading2'])) |
|
for entry in st.session_state.story: |
|
if entry['role'] == 'You': |
|
text = f"👤 {entry['content']}" |
|
if entry.get('is_correct'): |
|
style = ParagraphStyle( |
|
'Correct', |
|
parent=story_style, |
|
textColor=colors.darkgreen |
|
) |
|
else: |
|
style = story_style |
|
else: |
|
text = f"🤖 {entry['content']}" |
|
style = ParagraphStyle( |
|
'AI', |
|
parent=story_style, |
|
textColor=colors.navy |
|
) |
|
elements.append(Paragraph(text, style)) |
|
|
|
|
|
if hasattr(st.session_state, 'achievements') and st.session_state.achievements: |
|
elements.append(Spacer(1, 20)) |
|
elements.append(Paragraph("Achievements Earned", styles['Heading2'])) |
|
for achievement in st.session_state.achievements: |
|
elements.append(Paragraph(f"🏆 {achievement}", stats_style)) |
|
|
|
|
|
doc.build(elements) |
|
pdf = buffer.getvalue() |
|
buffer.close() |
|
return pdf |
|
|
|
|
|
def load_progress(uploaded_file): |
|
"""โหลดความก้าวหน้าจากไฟล์ JSON""" |
|
try: |
|
data = json.loads(uploaded_file.getvalue()) |
|
st.session_state.level = data['level'] |
|
st.session_state.story = data['story'] |
|
st.session_state.achievements = data['achievements'] |
|
st.session_state.points = data['points'] |
|
st.session_state.stats = data['stats'] |
|
st.session_state.stats['vocabulary_used'] = set(data['stats']['vocabulary_used']) |
|
st.success("โหลดความก้าวหน้าเรียบร้อย!") |
|
st.rerun() |
|
except Exception as e: |
|
st.error(f"เกิดข้อผิดพลาดในการโหลดไฟล์: {str(e)}") |
|
|
|
def reset_story(): |
|
"""Reset the story and related state variables.""" |
|
st.session_state.story = [] |
|
st.session_state.feedback = None |
|
st.session_state.unique_words = set() |
|
st.session_state.total_words = 0 |
|
st.session_state.achievements = [] |
|
st.session_state.should_reset = False |
|
|
|
|
|
st.session_state.points = { |
|
'total': 0, |
|
'perfect_sentences': 0, |
|
'corrections_made': 0, |
|
'streak': 0, |
|
'max_streak': 0 |
|
} |
|
|
|
|
|
st.session_state.stats = { |
|
'total_sentences': 0, |
|
'correct_first_try': 0, |
|
'accuracy_rate': 0.0, |
|
'vocabulary_used': set() |
|
} |
|
|
|
|
|
if st.session_state.should_reset: |
|
reset_story() |
|
|
|
|
|
st.markdown("# 📖 JoyStory") |
|
show_welcome_section() |
|
show_parent_guide() |
|
|
|
|
|
with st.sidebar: |
|
st.markdown("### 📂 โหลดความก้าวหน้า") |
|
uploaded_file = st.file_uploader( |
|
"เลือกไฟล์ .json", |
|
type=['json'], |
|
help="เลือกไฟล์ความก้าวหน้าที่บันทึกไว้" |
|
) |
|
if uploaded_file: |
|
if st.button("โหลดความก้าวหน้า"): |
|
load_progress(uploaded_file) |
|
|
|
st.markdown(""" |
|
<div class="thai"> |
|
🎯 เลือกระดับการเรียนรู้ |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
level = st.radio( |
|
"ระดับการเรียน", |
|
options=list(level_options.keys()), |
|
format_func=lambda x: level_options[x]['thai_name'], |
|
label_visibility="collapsed" |
|
) |
|
|
|
|
|
st.markdown(f""" |
|
<div class="level-info thai"> |
|
🎓 {level_options[level]['description']} |
|
<br>📚 เหมาะสำหรับอายุ: {level_options[level]['age_range']} |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
st.session_state.level = level |
|
|
|
if st.button("เริ่มเรื่องใหม่ | Start New Story"): |
|
st.session_state.should_reset = True |
|
st.rerun() |
|
|
|
|
|
|
|
col1, col2 = st.columns([3, 1]) |
|
|
|
with col1: |
|
|
|
st.markdown(""" |
|
<div class="thai-eng"> |
|
<div class="thai">📖 เรื่องราวของคุณ</div> |
|
<div class="eng">Your Story</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
story_display = st.container() |
|
|
|
with story_display: |
|
if not st.session_state.story: |
|
st.info("เริ่มต้นผจญภัยด้วยการเขียนประโยคแรกกันเลย! | Start your adventure by writing the first sentence!") |
|
else: |
|
for idx, entry in enumerate(st.session_state.story): |
|
if entry['role'] == 'You': |
|
|
|
status_icon = "✅ " if entry.get('is_correct') else "✍️ " |
|
st.write(f"👤 You: {status_icon}{entry['content']}") |
|
elif entry['role'] == 'AI': |
|
st.write("🤖 AI:", entry['content']) |
|
|
|
|
|
st.markdown(""" |
|
<div class="thai-eng"> |
|
<div class="thai">✏️ ถึงตาคุณแล้ว</div> |
|
<div class="eng">Your Turn</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.text_area( |
|
"เขียนต่อจากเรื่องราว | Continue the story:", |
|
height=100, |
|
key="text_input", |
|
label_visibility="collapsed" |
|
) |
|
|
|
|
|
st.button( |
|
"ส่งคำตอบ | Submit", |
|
on_click=submit_story |
|
) |
|
|
|
with col2: |
|
if st.session_state.feedback: |
|
st.markdown(""" |
|
<div class="thai-eng"> |
|
<div class="thai">📝 คำแนะนำจากครู</div> |
|
<div class="eng">Writing Feedback</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
feedback_data = st.session_state.feedback |
|
if isinstance(feedback_data, dict) and feedback_data.get('has_errors'): |
|
st.markdown(f""" |
|
<div style='background-color: #f0f2f6; |
|
padding: 15px; |
|
border-radius: 8px; |
|
border-left: 4px solid #FF9800; |
|
margin: 5px 0; |
|
font-family: "Sarabun", sans-serif;'> |
|
<p style='color: #1e88e5; margin-bottom: 10px;'> |
|
{feedback_data['feedback']} |
|
</p> |
|
<p style='color: #666; font-size: 0.9em;'> |
|
ประโยคที่ถูกต้อง:<br/> |
|
<span style='color: #4CAF50; font-weight: bold;'> |
|
{feedback_data['corrected']} |
|
</span> |
|
</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if st.button("✍️ แก้ไขประโยคให้ถูกต้อง", key="correct_button"): |
|
last_user_entry_idx = next( |
|
(i for i, entry in reversed(list(enumerate(st.session_state.story))) |
|
if entry['role'] == 'You'), |
|
None |
|
) |
|
if last_user_entry_idx is not None: |
|
apply_correction( |
|
last_user_entry_idx, |
|
feedback_data['corrected'] |
|
) |
|
st.rerun() |
|
else: |
|
st.markdown(f""" |
|
<div style='background-color: #f0f2f6; |
|
padding: 15px; |
|
border-radius: 8px; |
|
border-left: 4px solid #4CAF50; |
|
margin: 5px 0; |
|
font-family: "Sarabun", sans-serif;'> |
|
<p style='color: #1e88e5;'> |
|
{feedback_data.get('feedback', '✨ เขียนได้ถูกต้องแล้วค่ะ!')} |
|
</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown(""" |
|
<div class="thai-eng"> |
|
<div class="thai">✨ เครื่องมือช่วยเขียน</div> |
|
<div class="eng">Writing Tools</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
if st.button("ดูคำศัพท์แนะนำ | Get Vocabulary Ideas"): |
|
vocab_suggestions = get_vocabulary_suggestions() |
|
st.markdown("#### 📚 คำศัพท์น่ารู้ | Useful Words") |
|
for word in vocab_suggestions: |
|
st.markdown(f"• {word}") |
|
|
|
if st.button("ขอคำใบ้ | Get Creative Prompt"): |
|
prompt = get_creative_prompt() |
|
st.markdown(f""" |
|
<div class="thai-eng"> |
|
<div class="thai">💭 {prompt['thai']}</div> |
|
<div class="eng">💭 {prompt['eng']}</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
st.markdown("---") |
|
show_achievements() |
|
|
|
|
|
st.markdown(""" |
|
<div class="thai-eng"> |
|
<div class="thai">🏆 ความสำเร็จ</div> |
|
<div class="eng">Achievements</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if st.session_state.story: |
|
st.markdown("### 💾 บันทึกเรื่องราว") |
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
|
|
pdf = create_story_pdf() |
|
st.download_button( |
|
label="📑 ดาวน์โหลดเป็น PDF", |
|
data=pdf, |
|
file_name=f"my_story_{datetime.now().strftime('%Y%m%d')}.pdf", |
|
mime="application/pdf" |
|
) |
|
|
|
with col2: |
|
|
|
story_data = { |
|
'level': st.session_state.level, |
|
'date': datetime.now().isoformat(), |
|
'story': st.session_state.story, |
|
'achievements': st.session_state.achievements, |
|
'points': st.session_state.points, |
|
'stats': { |
|
'total_sentences': st.session_state.stats['total_sentences'], |
|
'correct_first_try': st.session_state.stats['correct_first_try'], |
|
'accuracy_rate': st.session_state.stats['accuracy_rate'], |
|
'vocabulary_used': list(st.session_state.stats['vocabulary_used']) |
|
} |
|
} |
|
st.download_button( |
|
label="💾 บันทึกความก้าวหน้า", |
|
data=json.dumps(story_data, indent=2), |
|
file_name=f"story_progress_{datetime.now().strftime('%Y%m%d')}.json", |
|
mime="application/json" |
|
) |