This commit is contained in:
silverpro89
2026-01-26 20:23:08 +07:00
parent 53d97ba5db
commit 2c7b4675a7
49 changed files with 12668 additions and 1 deletions

1240
GRAMMAR_GUIDE.md Normal file

File diff suppressed because it is too large Load Diff

1539
LEARNING_CONTENT_GUIDE.md Normal file

File diff suppressed because it is too large Load Diff

851
STORY_GUIDE.md Normal file
View File

@@ -0,0 +1,851 @@
# Story System Guide
**Version:** 1.0.0
**Last Updated:** 2026-01-26
**Purpose:** Complete guide for managing interactive stories in the SENA Language Learning System
---
## Table of Contents
1. [Overview](#overview)
2. [Database Structure](#database-structure)
3. [API Endpoints](#api-endpoints)
4. [Story Structure](#story-structure)
5. [Context Objects](#context-objects)
6. [Vocabulary Integration](#vocabulary-integration)
7. [Grade Levels & Tags](#grade-levels--tags)
8. [Use Cases & Examples](#use-cases--examples)
9. [Database Queries](#database-queries)
10. [Validation Rules](#validation-rules)
11. [AI Integration Guide](#ai-integration-guide)
---
## Overview
The Story System provides **interactive multimedia stories** for language learning. Each story combines:
- **Text Content**: Narrative text broken into scenes
- **Visual Media**: Images for each scene
- **Audio Narration**: Voice recordings for listening practice
- **Vocabulary Links**: Connect to existing vocabulary entries
- **Grade Targeting**: Assign appropriate difficulty levels
- **Tag Categorization**: Organize by themes, subjects, and skills
### Key Features
**Scene-Based Structure** - Stories broken into sequential contexts
**Multimedia Support** - Images and audio for each scene
**Vocabulary Integration** - Link to vocab entries for word learning
**Grade Filtering** - Target specific grade levels
**Tag System** - Flexible categorization and search
**UUID Security** - Prevent crawler attacks
**AI-Ready** - Comprehensive guide for automated content creation
---
## Database Structure
### Table: `stories`
| Column | Type | Required | Description |
|--------|------|----------|-------------|
| `id` | UUID | ✓ | Primary key (auto-generated) |
| `name` | STRING(255) | ✓ | Story title/name |
| `logo` | TEXT | - | URL to story logo/thumbnail image |
| `vocabulary` | JSON | - | Array of vocabulary words (default: []) |
| `context` | JSON | - | Array of story scenes (default: []) |
| `grade` | JSON | - | Array of grade levels (default: []) |
| `tag` | JSON | - | Array of tags (default: []) |
| `created_at` | TIMESTAMP | ✓ | Creation timestamp |
| `updated_at` | TIMESTAMP | ✓ | Last update timestamp |
**Indexes:**
- `idx_name` - Fast search by story name
- `idx_created_at` - Sort by creation date
---
## API Endpoints
Base URL: `/api/stories`
### 1. Create Story
**POST** `/api/stories`
**Headers:**
```
Authorization: Bearer <token>
Content-Type: application/json
```
**Request Body:**
```json
{
"name": "The Greedy Cat",
"logo": "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
"vocabulary": ["cat", "eat", "apple", "happy", "greedy"],
"context": [
{
"image": "https://cdn.sena.tech/story/scene1.jpg",
"text": "Once upon a time, there was a greedy cat.",
"audio": "https://cdn.sena.tech/audio/scene1.mp3",
"order": 1
},
{
"image": "https://cdn.sena.tech/story/scene2.jpg",
"text": "The cat loved eating apples every day.",
"audio": "https://cdn.sena.tech/audio/scene2.mp3",
"order": 2
}
],
"grade": ["Grade 1", "Grade 2"],
"tag": ["animals", "food", "lesson", "health", "fiction"]
}
```
**Response (201):**
```json
{
"success": true,
"message": "Story created successfully",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "The Greedy Cat",
"logo": "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
"vocabulary": ["cat", "eat", "apple", "happy", "greedy"],
"context": [...],
"grade": ["Grade 1", "Grade 2"],
"tag": ["animals", "food", "lesson", "health", "fiction"],
"created_at": "2026-01-26T10:00:00Z",
"updated_at": "2026-01-26T10:00:00Z"
}
}
```
### 2. Get All Stories
**GET** `/api/stories?page=1&limit=20&grade_filter=Grade 1&tag_filter=animals&search=cat`
**Query Parameters:**
- `page` (integer, default: 1)
- `limit` (integer, default: 20)
- `search` (string) - Search in story name
- `grade_filter` (string) - Filter by grade level
- `tag_filter` (string) - Filter by tag
- `sort_by` (string, default: 'created_at')
- `sort_order` ('ASC'/'DESC', default: 'DESC')
**Response (200):**
```json
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "The Greedy Cat",
"logo": "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
"grade": ["Grade 1", "Grade 2"],
"tag": ["animals", "food"],
"created_at": "2026-01-26T10:00:00Z",
"updated_at": "2026-01-26T10:00:00Z"
}
],
"pagination": {
"total": 50,
"page": 1,
"limit": 20,
"totalPages": 3
}
}
```
### 3. Get Story by ID
**GET** `/api/stories/:id`
**Response (200):**
```json
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "The Greedy Cat",
"logo": "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
"vocabulary": ["cat", "eat", "apple"],
"context": [...],
"grade": ["Grade 1", "Grade 2"],
"tag": ["animals", "food"],
"created_at": "2026-01-26T10:00:00Z",
"updated_at": "2026-01-26T10:00:00Z"
}
}
```
### 4. Update Story
**PUT** `/api/stories/:id`
**Request Body:** (partial update)
```json
{
"name": "The Greedy Cat (Updated)",
"tag": ["animals", "food", "lesson", "health", "fiction", "updated"]
}
```
**Response (200):**
```json
{
"success": true,
"message": "Story updated successfully",
"data": {...}
}
```
### 5. Delete Story
**DELETE** `/api/stories/:id`
**Response (200):**
```json
{
"success": true,
"message": "Story deleted successfully"
}
```
### 6. Get Stories by Grade
**GET** `/api/stories/grade?grade=Grade 1`
**Response (200):**
```json
{
"success": true,
"data": [...],
"count": 15
}
```
### 7. Get Stories by Tag
**GET** `/api/stories/tag?tag=animals`
**Response (200):**
```json
{
"success": true,
"data": [...],
"count": 12
}
```
### 8. Get Story Statistics
**GET** `/api/stories/stats`
**Response (200):**
```json
{
"success": true,
"data": {
"total": 50,
"by_grade": [
{ "grade": "Grade 1", "count": 20 },
{ "grade": "Grade 2", "count": 15 }
],
"by_tag": [
{ "tag": "animals", "count": 12 },
{ "tag": "food", "count": 8 }
]
}
}
```
### 9. Get Story Guide (for AI)
**GET** `/api/stories/guide`
Returns comprehensive guide in JSON format for AI agents.
---
## Story Structure
### Minimal Story
```json
{
"name": "My Pet Dog",
"context": [
{ "text": "I have a pet dog.", "order": 1 },
{ "text": "My dog is brown.", "order": 2 },
{ "text": "I love my dog.", "order": 3 }
]
}
```
### Complete Story
```json
{
"name": "The Greedy Cat",
"logo": "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
"vocabulary": ["cat", "eat", "apple", "happy", "greedy"],
"context": [
{
"image": "https://cdn.sena.tech/story/gc-scene1.jpg",
"text": "Once upon a time, there was a greedy cat.",
"audio": "https://cdn.sena.tech/audio/gc-scene1.mp3",
"order": 1
},
{
"image": "https://cdn.sena.tech/story/gc-scene2.jpg",
"text": "The cat loved eating apples every day.",
"audio": "https://cdn.sena.tech/audio/gc-scene2.mp3",
"order": 2
},
{
"image": "https://cdn.sena.tech/story/gc-scene3.jpg",
"text": "One day, the cat ate too many apples and felt sick.",
"audio": "https://cdn.sena.tech/audio/gc-scene3.mp3",
"order": 3
},
{
"image": "https://cdn.sena.tech/story/gc-scene4.jpg",
"text": "The cat learned to eat just enough and was happy again.",
"audio": "https://cdn.sena.tech/audio/gc-scene4.mp3",
"order": 4
}
],
"grade": ["Grade 1", "Grade 2"],
"tag": ["animals", "food", "lesson", "health", "fiction"]
}
```
---
## Context Objects
Each context object represents a **scene or page** in the story.
### Context Structure
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `text` | string | ✓ | Story text for this scene |
| `order` | integer | ✓ | Sequence number (1, 2, 3...) |
| `image` | string (URL) | - | Scene illustration image |
| `audio` | string (URL) | - | Audio narration file |
### Example Context
```json
{
"image": "https://cdn.sena.tech/story/scene1.jpg",
"text": "Once upon a time, there was a greedy cat.",
"audio": "https://cdn.sena.tech/audio/scene1.mp3",
"order": 1
}
```
### Best Practices
**Sequential Order** - Use 1, 2, 3, 4... for logical flow
**Concise Text** - Keep sentences short and age-appropriate
**High-Quality Images** - Use clear, engaging illustrations
**Audio Narration** - Provide audio for listening practice
**Consistent Style** - Maintain uniform visual and audio quality
---
## Vocabulary Integration
### Linking to Vocab Entries
**Option 1: Simple Word List**
```json
{
"vocabulary": ["cat", "eat", "apple", "happy"]
}
```
**Option 2: Vocab Codes (Recommended)**
```json
{
"vocabulary": ["vocab-001-cat", "vocab-002-eat", "vocab-015-apple"]
}
```
### Benefits
- **Word Learning** - Students learn vocabulary through context
- **Practice** - Reinforce words from vocabulary lessons
- **Tracking** - Monitor which words appear in which stories
- **Search** - Find stories containing specific vocabulary
### Guidelines
✅ Include only words that **appear in the story**
✅ Use vocab_code from Vocab table for better tracking
✅ Order by frequency or importance
✅ Limit to 5-15 key words per story
---
## Grade Levels & Tags
### Supported Grade Levels
```json
{
"grade": ["Pre-K", "Kindergarten", "Grade 1", "Grade 2", "Grade 3", "Grade 4", "Grade 5", "Grade 6"]
}
```
**Tips:**
- Can assign **multiple grades** for cross-level content
- Consider vocabulary difficulty when assigning grades
- Match with curriculum standards
### Tag Categories
**Themes:**
```json
["adventure", "friendship", "family", "courage", "honesty", "kindness"]
```
**Subjects:**
```json
["animals", "nature", "food", "school", "home", "travel"]
```
**Skills:**
```json
["reading", "listening", "vocabulary", "grammar", "phonics"]
```
**Emotions:**
```json
["happy", "sad", "excited", "scared", "surprised"]
```
**Genres:**
```json
["fiction", "non-fiction", "fantasy", "realistic", "fable"]
```
### Tag Best Practices
✅ Use **3-7 tags** per story
✅ Mix different category types
✅ Keep tags **consistent** across stories
✅ Use **lowercase** for consistency
✅ Avoid redundant tags
---
## Use Cases & Examples
### Use Case 1: Animal Story for Grade 1
```json
{
"name": "The Brave Little Mouse",
"logo": "https://cdn.sena.tech/thumbs/brave-mouse.jpg",
"vocabulary": ["mouse", "brave", "help", "friend"],
"context": [
{
"image": "https://cdn.sena.tech/mouse/scene1.jpg",
"text": "There was a little mouse.",
"audio": "https://cdn.sena.tech/mouse/audio1.mp3",
"order": 1
},
{
"image": "https://cdn.sena.tech/mouse/scene2.jpg",
"text": "The mouse was very brave.",
"audio": "https://cdn.sena.tech/mouse/audio2.mp3",
"order": 2
},
{
"image": "https://cdn.sena.tech/mouse/scene3.jpg",
"text": "The mouse helped his friend.",
"audio": "https://cdn.sena.tech/mouse/audio3.mp3",
"order": 3
}
],
"grade": ["Grade 1"],
"tag": ["animals", "courage", "friendship", "fiction"]
}
```
### Use Case 2: Food Story for Multiple Grades
```json
{
"name": "The Healthy Eating Adventure",
"logo": "https://cdn.sena.tech/thumbs/healthy-eating.jpg",
"vocabulary": ["apple", "orange", "banana", "healthy", "eat"],
"context": [
{
"image": "https://cdn.sena.tech/food/scene1.jpg",
"text": "Lisa loves to eat healthy food.",
"audio": "https://cdn.sena.tech/food/audio1.mp3",
"order": 1
},
{
"image": "https://cdn.sena.tech/food/scene2.jpg",
"text": "She eats apples, oranges, and bananas every day.",
"audio": "https://cdn.sena.tech/food/audio2.mp3",
"order": 2
},
{
"image": "https://cdn.sena.tech/food/scene3.jpg",
"text": "Eating healthy makes her strong and happy!",
"audio": "https://cdn.sena.tech/food/audio3.mp3",
"order": 3
}
],
"grade": ["Grade 1", "Grade 2", "Grade 3"],
"tag": ["food", "health", "lesson", "realistic"]
}
```
### Use Case 3: Educational Non-Fiction
```json
{
"name": "How Plants Grow",
"logo": "https://cdn.sena.tech/thumbs/plants-grow.jpg",
"vocabulary": ["seed", "water", "sun", "grow", "plant"],
"context": [
{
"image": "https://cdn.sena.tech/plant/scene1.jpg",
"text": "A plant starts from a tiny seed.",
"audio": "https://cdn.sena.tech/plant/audio1.mp3",
"order": 1
},
{
"image": "https://cdn.sena.tech/plant/scene2.jpg",
"text": "The seed needs water and sun to grow.",
"audio": "https://cdn.sena.tech/plant/audio2.mp3",
"order": 2
},
{
"image": "https://cdn.sena.tech/plant/scene3.jpg",
"text": "Soon, it becomes a big, beautiful plant!",
"audio": "https://cdn.sena.tech/plant/audio3.mp3",
"order": 3
}
],
"grade": ["Grade 2", "Grade 3"],
"tag": ["nature", "science", "non-fiction", "plants"]
}
```
---
## Database Queries
### Query 1: Find All Stories for Grade 1
```sql
SELECT * FROM stories
WHERE grade::jsonb @> '["Grade 1"]'::jsonb
ORDER BY created_at DESC;
```
### Query 2: Find Stories by Tag
```sql
SELECT * FROM stories
WHERE tag::jsonb @> '["animals"]'::jsonb
ORDER BY name ASC;
```
### Query 3: Search Stories by Name
```sql
SELECT * FROM stories
WHERE name LIKE '%cat%'
ORDER BY created_at DESC;
```
### Query 4: Get Stories with Specific Vocabulary
```sql
SELECT * FROM stories
WHERE vocabulary::jsonb @> '["cat"]'::jsonb
ORDER BY created_at DESC;
```
### Query 5: Count Stories by Grade
```sql
SELECT
grade_elem AS grade,
COUNT(*) AS story_count
FROM stories,
jsonb_array_elements_text(grade) AS grade_elem
GROUP BY grade_elem
ORDER BY story_count DESC;
```
### Query 6: Count Stories by Tag
```sql
SELECT
tag_elem AS tag,
COUNT(*) AS story_count
FROM stories,
jsonb_array_elements_text(tag) AS tag_elem
GROUP BY tag_elem
ORDER BY story_count DESC;
```
---
## Validation Rules
### 1. Required Fields
**Rule:** `name` is required
**Valid:**
```json
{ "name": "My Story" }
```
**Invalid:**
```json
{ "logo": "..." }
```
### 2. Context Structure
**Rule:** Each context must have `text` and `order`
**Valid:**
```json
{
"context": [
{ "text": "Scene 1", "order": 1 },
{ "text": "Scene 2", "order": 2 }
]
}
```
**Invalid:**
```json
{
"context": [
{ "text": "Scene 1" } // Missing order
]
}
```
### 3. Sequential Order
**Rule:** Order numbers should be sequential
**Valid:**
```json
{
"context": [
{ "order": 1, "text": "..." },
{ "order": 2, "text": "..." },
{ "order": 3, "text": "..." }
]
}
```
**Invalid:**
```json
{
"context": [
{ "order": 1, "text": "..." },
{ "order": 3, "text": "..." }, // Skipped 2
{ "order": 2, "text": "..." } // Out of order
]
}
```
### 4. Array Fields
**Rule:** `grade`, `tag`, `vocabulary`, `context` must be arrays
**Valid:**
```json
{
"grade": ["Grade 1"],
"tag": ["animals"]
}
```
**Invalid:**
```json
{
"grade": "Grade 1", // Should be array
"tag": "animals" // Should be array
}
```
### 5. URL Validation
**Rule:** `logo`, `image`, `audio` should be valid URLs
**Valid:**
```json
{
"logo": "https://cdn.sena.tech/logo.jpg",
"context": [
{
"image": "https://cdn.sena.tech/scene.jpg",
"audio": "https://cdn.sena.tech/audio.mp3",
"text": "...",
"order": 1
}
]
}
```
---
## AI Integration Guide
### For AI Agents Creating Stories
#### Step 1: Choose Story Topic
1. Identify target audience (grade level)
2. Select theme (animals, food, adventure, etc.)
3. Determine vocabulary focus
4. Decide story length (3-10 scenes)
#### Step 2: Create Story Outline
```
Title: The Greedy Cat
Grade: Grade 1-2
Theme: Animals + Health Lesson
Vocabulary: cat, eat, apple, greedy, sick, happy
Scenes: 4
```
#### Step 3: Write Context Scenes
For each scene:
1. Write concise, age-appropriate text
2. Assign sequential order number
3. Plan corresponding image
4. Prepare audio narration
#### Step 4: Structure Data
```json
{
"name": "The Greedy Cat",
"logo": "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
"vocabulary": ["cat", "eat", "apple", "greedy", "sick", "happy"],
"context": [
{
"image": "https://cdn.sena.tech/story/gc-scene1.jpg",
"text": "Once upon a time, there was a greedy cat.",
"audio": "https://cdn.sena.tech/audio/gc-scene1.mp3",
"order": 1
}
// ... more scenes
],
"grade": ["Grade 1", "Grade 2"],
"tag": ["animals", "food", "lesson", "health", "fiction"]
}
```
#### Step 5: Validate Data
**Pre-Submit Checklist:**
1.`name` is provided and descriptive
2.`context` array has at least 1 scene
3. ✓ Each context has `text` and `order`
4. ✓ Order numbers are sequential (1, 2, 3...)
5. ✓ Vocabulary words match story content
6. ✓ Grade levels are appropriate
7. ✓ Tags are relevant and descriptive
8. ✓ URLs are accessible
9. ✓ Text is age-appropriate
10. ✓ Story has clear beginning, middle, end
#### Step 6: Submit via API
```bash
POST /api/stories
Content-Type: application/json
Authorization: Bearer <token>
{...story data...}
```
### Common Mistakes to Avoid
1. **Missing Context Order**
-`{ "context": [{ "text": "Some text" }] }`
-`{ "context": [{ "text": "Some text", "order": 1 }] }`
2. **Non-Sequential Order**
-`[{ "order": 1 }, { "order": 3 }, { "order": 2 }]`
-`[{ "order": 1 }, { "order": 2 }, { "order": 3 }]`
3. **Invalid Grade Format**
-`{ "grade": "Grade 1" }`
-`{ "grade": ["Grade 1"] }`
4. **Mismatched Vocabulary**
-`{ "name": "The Cat", "vocabulary": ["dog", "bird"] }`
-`{ "name": "The Cat", "vocabulary": ["cat"] }`
5. **Missing Story Name**
-`{ "context": [...] }`
-`{ "name": "My Story", "context": [...] }`
### AI Tips
- **Content Creation:** Generate age-appropriate, engaging stories with clear moral lessons
- **Vocabulary Integration:** Reference existing vocabulary entries from Vocab table
- **Multimedia:** Always provide audio URLs for better learning engagement
- **Sequencing:** Ensure context order is logical and sequential
- **Testing:** Test story flow by reading context in order
- **Consistency:** Use consistent naming conventions for URLs and vocabulary
---
## Summary
The Story System provides a comprehensive framework for:
1. **Creating Interactive Stories** - Multimedia learning content
2. **Vocabulary Integration** - Link to existing vocab entries
3. **Grade Targeting** - Assign appropriate difficulty levels
4. **Flexible Categorization** - Organize by themes, subjects, skills
5. **AI-Ready** - Comprehensive guide for automated content creation
**Key Benefits:**
- ✅ Engaging multimedia learning experience
- ✅ Vocabulary reinforcement through context
- ✅ Grade-appropriate content filtering
- ✅ Flexible tag-based organization
- ✅ UUID security to prevent crawlers
- ✅ AI-friendly for automated story generation
**Next Steps:**
1. Create story content with context scenes
2. Link vocabulary from Vocab table
3. Assign appropriate grades and tags
4. Provide multimedia assets (images, audio)
5. Test story flow and engagement
---
**For questions or support, contact the SENA development team.**

1577
VOCAB_GUIDE.md Normal file

File diff suppressed because it is too large Load Diff

9
app.js
View File

@@ -34,6 +34,10 @@ const chapterRoutes = require('./routes/chapterRoutes');
const gameRoutes = require('./routes/gameRoutes');
const lessonRoutes = require('./routes/lessonRoutes');
const chapterLessonRoutes = require('./routes/chapterLessonRoutes');
const vocabRoutes = require('./routes/vocabRoutes');
const grammarRoutes = require('./routes/grammarRoutes');
const storyRoutes = require('./routes/storyRoutes');
const learningContentRoutes = require('./routes/learningContentRoutes');
/**
* Initialize Express Application
@@ -146,6 +150,7 @@ app.get('/api', (req, res) => {
chapters: '/api/chapters',
lessons: '/api/lessons',
games: '/api/games',
vocab: '/api/vocab',
},
documentation: '/api-docs',
});
@@ -198,6 +203,10 @@ app.use('/api/chapters', chapterRoutes);
app.use('/api/chapters', chapterLessonRoutes); // Nested route: /api/chapters/:id/lessons
app.use('/api/games', gameRoutes);
app.use('/api/lessons', lessonRoutes);
app.use('/api/vocab', vocabRoutes);
app.use('/api/grammar', grammarRoutes);
app.use('/api/stories', storyRoutes);
app.use('/api/learning-content', learningContentRoutes);
/**
* Queue Status Endpoint

View File

@@ -0,0 +1,948 @@
const { Grammar, GrammarMapping, GrammarMediaStory, sequelize } = require('../models');
const { Op } = require('sequelize');
/**
* CREATE: Add new grammar rule with mappings and media stories
*/
exports.createGrammar = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const {
grammar_code,
title,
translation,
structure,
instructions,
difficulty_score,
category,
tags,
mappings = [],
media_stories = []
} = req.body;
// Validate required fields
if (!grammar_code || !title || !structure) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Missing required fields: grammar_code, title, structure'
});
}
// Validate structure has formula
if (!structure.formula) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'structure.formula is required'
});
}
// Check if grammar_code already exists
const existing = await Grammar.findOne({ where: { grammar_code } });
if (existing) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: `Grammar code "${grammar_code}" already exists`
});
}
// Create main grammar entry
const grammar = await Grammar.create({
grammar_code,
title,
translation,
structure,
instructions,
difficulty_score,
category,
tags
}, { transaction });
// Create mappings if provided
if (mappings.length > 0) {
const mappingData = mappings.map(m => ({
grammar_id: grammar.id,
book_id: m.book_id,
grade: m.grade,
unit: m.unit,
lesson: m.lesson,
context_note: m.context_note
}));
await GrammarMapping.bulkCreate(mappingData, { transaction });
}
// Create media stories if provided
if (media_stories.length > 0) {
const storyData = media_stories.map(s => ({
grammar_id: grammar.id,
story_id: s.story_id,
title: s.title,
type: s.type || 'story',
url: s.url,
thumbnail: s.thumbnail,
description: s.description,
duration_seconds: s.duration_seconds,
min_grade: s.min_grade
}));
await GrammarMediaStory.bulkCreate(storyData, { transaction });
}
await transaction.commit();
// Fetch complete grammar with associations
const result = await Grammar.findByPk(grammar.id, {
include: [
{ model: GrammarMapping, as: 'mappings' },
{ model: GrammarMediaStory, as: 'mediaStories' }
]
});
res.status(201).json({
success: true,
message: 'Grammar rule created successfully',
data: result
});
} catch (error) {
await transaction.rollback();
console.error('Error creating grammar:', error);
res.status(500).json({
success: false,
message: 'Failed to create grammar rule',
error: error.message
});
}
};
/**
* READ: Get all grammars with pagination and filters
*/
exports.getAllGrammars = async (req, res) => {
try {
const {
page = 1,
limit = 20,
category,
grade,
book_id,
difficulty_min,
difficulty_max,
search,
include_media = 'false'
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(limit);
const where = { is_active: true };
// Apply filters
if (category) {
where.category = category;
}
if (difficulty_min || difficulty_max) {
where.difficulty_score = {};
if (difficulty_min) where.difficulty_score[Op.gte] = parseInt(difficulty_min);
if (difficulty_max) where.difficulty_score[Op.lte] = parseInt(difficulty_max);
}
if (search) {
where[Op.or] = [
{ title: { [Op.like]: `%${search}%` } },
{ translation: { [Op.like]: `%${search}%` } },
{ grammar_code: { [Op.like]: `%${search}%` } }
];
}
// Build include array
const include = [
{
model: GrammarMapping,
as: 'mappings',
required: false,
where: {}
}
];
// Add grade/book_id filter to mappings
if (grade) {
include[0].where.grade = parseInt(grade);
include[0].required = true;
}
if (book_id) {
include[0].where.book_id = book_id;
include[0].required = true;
}
// Include media stories if requested
if (include_media === 'true') {
include.push({
model: GrammarMediaStory,
as: 'mediaStories',
required: false
});
}
const { count, rows } = await Grammar.findAndCountAll({
where,
include,
limit: parseInt(limit),
offset,
order: [['difficulty_score', 'ASC'], ['createdAt', 'DESC']],
distinct: true
});
res.json({
success: true,
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching grammars:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch grammars',
error: error.message
});
}
};
/**
* READ: Get grammar by ID or grammar_code
*/
exports.getGrammarById = async (req, res) => {
try {
const { id } = req.params;
// Determine if id is numeric or string (grammar_code)
const where = isNaN(id)
? { grammar_code: id, is_active: true }
: { id: parseInt(id), is_active: true };
const grammar = await Grammar.findOne({
where,
include: [
{ model: GrammarMapping, as: 'mappings' },
{ model: GrammarMediaStory, as: 'mediaStories' }
]
});
if (!grammar) {
return res.status(404).json({
success: false,
message: 'Grammar rule not found'
});
}
res.json({
success: true,
data: grammar
});
} catch (error) {
console.error('Error fetching grammar:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch grammar rule',
error: error.message
});
}
};
/**
* UPDATE: Update grammar rule
*/
exports.updateGrammar = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { id } = req.params;
const {
title,
translation,
structure,
instructions,
difficulty_score,
category,
tags,
mappings,
media_stories
} = req.body;
// Find grammar
const where = isNaN(id)
? { grammar_code: id, is_active: true }
: { id: parseInt(id), is_active: true };
const grammar = await Grammar.findOne({ where });
if (!grammar) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Grammar rule not found'
});
}
// Update main grammar fields
const updateData = {};
if (title !== undefined) updateData.title = title;
if (translation !== undefined) updateData.translation = translation;
if (structure !== undefined) {
if (!structure.formula) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'structure.formula is required'
});
}
updateData.structure = structure;
}
if (instructions !== undefined) updateData.instructions = instructions;
if (difficulty_score !== undefined) updateData.difficulty_score = difficulty_score;
if (category !== undefined) updateData.category = category;
if (tags !== undefined) updateData.tags = tags;
await grammar.update(updateData, { transaction });
// Update mappings if provided
if (mappings !== undefined) {
await GrammarMapping.destroy({ where: { grammar_id: grammar.id }, transaction });
if (mappings.length > 0) {
const mappingData = mappings.map(m => ({
grammar_id: grammar.id,
book_id: m.book_id,
grade: m.grade,
unit: m.unit,
lesson: m.lesson,
context_note: m.context_note
}));
await GrammarMapping.bulkCreate(mappingData, { transaction });
}
}
// Update media stories if provided
if (media_stories !== undefined) {
await GrammarMediaStory.destroy({ where: { grammar_id: grammar.id }, transaction });
if (media_stories.length > 0) {
const storyData = media_stories.map(s => ({
grammar_id: grammar.id,
story_id: s.story_id,
title: s.title,
type: s.type || 'story',
url: s.url,
thumbnail: s.thumbnail,
description: s.description,
duration_seconds: s.duration_seconds,
min_grade: s.min_grade
}));
await GrammarMediaStory.bulkCreate(storyData, { transaction });
}
}
await transaction.commit();
// Fetch updated grammar
const result = await Grammar.findByPk(grammar.id, {
include: [
{ model: GrammarMapping, as: 'mappings' },
{ model: GrammarMediaStory, as: 'mediaStories' }
]
});
res.json({
success: true,
message: 'Grammar rule updated successfully',
data: result
});
} catch (error) {
await transaction.rollback();
console.error('Error updating grammar:', error);
res.status(500).json({
success: false,
message: 'Failed to update grammar rule',
error: error.message
});
}
};
/**
* DELETE: Soft delete grammar
*/
exports.deleteGrammar = async (req, res) => {
try {
const { id } = req.params;
const where = isNaN(id)
? { grammar_code: id, is_active: true }
: { id: parseInt(id), is_active: true };
const grammar = await Grammar.findOne({ where });
if (!grammar) {
return res.status(404).json({
success: false,
message: 'Grammar rule not found'
});
}
await grammar.update({ is_active: false });
res.json({
success: true,
message: 'Grammar rule deleted successfully'
});
} catch (error) {
console.error('Error deleting grammar:', error);
res.status(500).json({
success: false,
message: 'Failed to delete grammar rule',
error: error.message
});
}
};
/**
* READ: Get grammars by curriculum mapping
*/
exports.getGrammarsByCurriculum = async (req, res) => {
try {
const { book_id, grade, unit, lesson } = req.query;
if (!book_id && !grade) {
return res.status(400).json({
success: false,
message: 'At least one of book_id or grade is required'
});
}
const mappingWhere = {};
if (book_id) mappingWhere.book_id = book_id;
if (grade) mappingWhere.grade = parseInt(grade);
if (unit) mappingWhere.unit = parseInt(unit);
if (lesson) mappingWhere.lesson = parseInt(lesson);
const grammars = await Grammar.findAll({
where: { is_active: true },
include: [
{
model: GrammarMapping,
as: 'mappings',
where: mappingWhere,
required: true
},
{
model: GrammarMediaStory,
as: 'mediaStories',
required: false
}
],
order: [['difficulty_score', 'ASC']]
});
res.json({
success: true,
data: grammars,
count: grammars.length
});
} catch (error) {
console.error('Error fetching grammars by curriculum:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch grammars by curriculum',
error: error.message
});
}
};
/**
* READ: Get grammar statistics
*/
exports.getGrammarStats = async (req, res) => {
try {
const totalGrammars = await Grammar.count({ where: { is_active: true } });
const byCategory = await Grammar.findAll({
where: { is_active: true },
attributes: [
'category',
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
group: ['category']
});
const byDifficulty = await Grammar.findAll({
where: { is_active: true },
attributes: [
'difficulty_score',
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
group: ['difficulty_score'],
order: [['difficulty_score', 'ASC']]
});
const byGrade = await Grammar.findAll({
where: { is_active: true },
include: [{
model: GrammarMapping,
as: 'mappings',
attributes: []
}],
attributes: [
[sequelize.col('mappings.grade'), 'grade'],
[sequelize.fn('COUNT', sequelize.fn('DISTINCT', sequelize.col('Grammar.id'))), 'count']
],
group: [sequelize.col('mappings.grade')],
raw: true
});
res.json({
success: true,
data: {
total: totalGrammars,
by_category: byCategory,
by_difficulty: byDifficulty,
by_grade: byGrade
}
});
} catch (error) {
console.error('Error fetching grammar stats:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch grammar statistics',
error: error.message
});
}
};
/**
* GET GUIDE: Comprehensive guide for AI to create grammar rules
*/
exports.getGrammarGuide = async (req, res) => {
try {
const guide = {
guide_version: "1.0.0",
last_updated: new Date().toISOString(),
data_structure: {
required_fields: {
grammar_code: {
type: "string(100)",
format: "gram-{sequence}-{identifier}",
example: "gram-001-present-cont",
description: "Unique identifier for grammar rule"
},
title: {
type: "string(200)",
example: "Present Continuous",
description: "Grammar rule name in English"
},
structure: {
type: "JSON object",
required_properties: {
formula: {
type: "string",
example: "S + am/is/are + V-ing + (a/an) + O + Adv",
description: "Formula showing sentence structure"
},
pattern_logic: {
type: "array of objects",
description: "Defines how to pick words from Vocabulary table",
item_structure: {
slot_id: "Unique slot identifier (e.g., S_01, V_01)",
role: "Word role from vocab.syntax (is_subject, is_verb, is_object, etc.)",
semantic_filter: "Array of semantic types to filter (e.g., ['human', 'animal'])",
use_form: "Which vocab form to use (v1, v_ing, v2, n_singular, etc.)",
dependency: "Slot ID this depends on for grammar agreement",
is_optional: "Boolean - whether this slot can be skipped",
position: "Where to place in sentence (start, middle, end)"
}
}
}
}
},
optional_fields: {
translation: {
type: "string(200)",
example: "Thì hiện tại tiếp diễn",
description: "Vietnamese translation"
},
instructions: {
type: "JSON object",
properties: {
vi: "Vietnamese instruction text",
hint: "Short hint for students"
},
example: {
vi: "Dùng để nói về hành động đang diễn ra.",
hint: "Cấu trúc: Be + V-ing"
}
},
difficulty_score: {
type: "integer",
range: "1-10",
default: 1,
description: "1 = easiest, 10 = hardest"
},
category: {
type: "string(100)",
examples: ["Tenses", "Modal Verbs", "Questions", "Conditionals", "Passive Voice"],
description: "Grammar category"
},
tags: {
type: "JSON array",
example: ["present", "continuous", "action"],
description: "Tags for search and categorization"
},
mappings: {
type: "array of objects",
description: "Curriculum mapping - where this grammar appears in textbooks",
item_structure: {
book_id: "Book identifier (e.g., 'global-success-2')",
grade: "Grade level (integer)",
unit: "Unit number (integer, optional)",
lesson: "Lesson number (integer, optional)",
context_note: "Additional context (string, optional)"
}
},
media_stories: {
type: "array of objects",
description: "Stories/videos demonstrating this grammar",
item_structure: {
story_id: "Unique story identifier (e.g., 'st-01')",
title: "Story title",
type: "Media type: 'story', 'video', 'animation', 'audio'",
url: "Single URL to complete media file (string, required)",
thumbnail: "Thumbnail image URL (optional)",
description: "Story description (optional)",
duration_seconds: "Duration in seconds (integer, optional)",
min_grade: "Minimum grade level (integer, optional)"
}
}
}
},
pattern_logic_roles: {
description: "Available roles to query from Vocab table syntax field",
roles: [
{ role: "is_subject", description: "Can be used as sentence subject", vocab_example: "I, cat, teacher" },
{ role: "is_verb", description: "Action or state verb", vocab_example: "eat, run, sleep" },
{ role: "is_object", description: "Can be used as object", vocab_example: "apple, book, water" },
{ role: "is_be", description: "Be verb (am/is/are)", vocab_example: "am, is, are" },
{ role: "is_adj", description: "Adjective", vocab_example: "happy, big, red" },
{ role: "is_adv", description: "Adverb", vocab_example: "quickly, slowly, happily" },
{ role: "is_article", description: "Article (a/an)", vocab_example: "a, an" },
{ role: "is_pronoun", description: "Pronoun", vocab_example: "I, you, he, she" },
{ role: "is_preposition", description: "Preposition", vocab_example: "in, on, at" }
]
},
semantic_filters: {
description: "Semantic types from vocab.semantics.can_be_subject_type or can_take_object_type",
common_types: [
"human", "animal", "object", "food", "plant", "place",
"abstract", "emotion", "action", "state", "container", "tool"
],
usage: "Use to ensure semantic compatibility (e.g., only humans can 'think', only food can be 'eaten')"
},
form_keys_reference: {
description: "Available form keys from VocabForm table",
verb_forms: ["v1", "v_s_es", "v_ing", "v2", "v3"],
noun_forms: ["n_singular", "n_plural"],
adjective_forms: ["adj_base", "adj_comparative", "adj_superlative"],
adverb_forms: ["adv_manner", "adv_frequency", "adv_time"],
pronoun_forms: ["pron_subject", "pron_object", "pron_possessive"]
},
rules: {
grammar_code_format: {
pattern: "gram-{3-digit-sequence}-{kebab-case-identifier}",
valid: ["gram-001-present-cont", "gram-002-past-simple", "gram-015-modal-can"],
invalid: ["gram-1-test", "GRAM-001-TEST", "present-continuous"]
},
formula_format: {
description: "Human-readable formula showing sentence structure",
tips: [
"Use S for Subject, V for Verb, O for Object, Adv for Adverb",
"Show alternatives with slash: am/is/are",
"Mark optional elements with parentheses: (Adv)",
"Keep it simple and educational"
],
examples: [
"S + V + O",
"S + am/is/are + V-ing",
"S + can/will/must + V1 + O",
"Wh-word + am/is/are + S + V-ing?"
]
},
pattern_logic_workflow: {
description: "How Grammar Engine uses pattern_logic to generate sentences",
steps: [
"1. Loop through each slot in pattern_logic array (in order)",
"2. Query Vocab table filtering by role (e.g., WHERE syntax->>'is_verb' = true)",
"3. Apply semantic_filter if specified (e.g., WHERE semantics->>'can_be_subject_type' @> '[\"human\"]')",
"4. If use_form specified, fetch that specific form from VocabForm table",
"5. If dependency specified, apply grammar agreement rules (e.g., match be verb with subject)",
"6. Randomly select one matching word",
"7. If is_optional = true and randomly skip, continue to next slot",
"8. Concatenate all selected words according to formula"
]
},
dependency_rules: {
be_verb_agreement: {
description: "Match am/is/are with subject",
logic: "If S_01 is 'I' → use 'am', if singular → 'is', if plural → 'are'"
},
article_selection: {
description: "Choose a/an based on next word's phonetic",
logic: "If O_01 starts with vowel sound → 'an', else → 'a'"
},
semantic_matching: {
description: "Ensure verb can take the object type",
logic: "If V_01.semantics.can_take_object_type includes O_01.semantics.word_type → valid"
}
}
},
examples: {
present_continuous: {
grammar_code: "gram-001-present-cont",
title: "Present Continuous",
translation: "Thì hiện tại tiếp diễn",
structure: {
formula: "S + am/is/are + V-ing + (a/an) + O + Adv",
pattern_logic: [
{
slot_id: "S_01",
role: "is_subject",
semantic_filter: ["human", "animal"]
},
{
slot_id: "BE_01",
role: "is_be",
dependency: "S_01"
},
{
slot_id: "V_01",
role: "is_verb",
use_form: "v_ing",
semantic_filter: ["action"]
},
{
slot_id: "ART_01",
role: "is_article",
dependency: "O_01",
is_optional: true
},
{
slot_id: "O_01",
role: "is_object",
semantic_match: "V_01"
},
{
slot_id: "ADV_01",
role: "is_adv",
is_optional: true,
position: "end"
}
]
},
instructions: {
vi: "Dùng để nói về hành động đang diễn ra.",
hint: "Cấu trúc: Be + V-ing"
},
difficulty_score: 2,
category: "Tenses",
tags: ["present", "continuous", "action"],
mappings: [
{
book_id: "global-success-2",
grade: 2,
unit: 5,
lesson: 1
}
],
media_stories: [
{
story_id: "st-01",
title: "The Greedy Cat",
type: "story",
url: "https://cdn.sena.tech/stories/the-greedy-cat-full.mp4",
thumbnail: "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
description: "A story about a cat who loves eating everything.",
duration_seconds: 180,
min_grade: 2
}
]
},
simple_question: {
grammar_code: "gram-010-yes-no-question",
title: "Yes/No Questions with Be",
translation: "Câu hỏi Yes/No với động từ Be",
structure: {
formula: "Am/Is/Are + S + Adj?",
pattern_logic: [
{
slot_id: "BE_01",
role: "is_be",
position: "start"
},
{
slot_id: "S_01",
role: "is_subject",
semantic_filter: ["human", "animal", "object"]
},
{
slot_id: "ADJ_01",
role: "is_adj"
}
]
},
instructions: {
vi: "Dùng để hỏi về trạng thái hoặc tính chất.",
hint: "Đảo Be lên đầu câu"
},
difficulty_score: 1,
category: "Questions",
tags: ["question", "be-verb", "beginner"],
mappings: [
{
book_id: "global-success-1",
grade: 1,
unit: 3,
lesson: 2
}
]
},
modal_can: {
grammar_code: "gram-020-modal-can",
title: "Modal Verb Can",
translation: "Động từ khiếm khuyết Can",
structure: {
formula: "S + can + V1 + O",
pattern_logic: [
{
slot_id: "S_01",
role: "is_subject",
semantic_filter: ["human", "animal"]
},
{
slot_id: "V_01",
role: "is_verb",
use_form: "v1",
semantic_filter: ["action", "state"]
},
{
slot_id: "O_01",
role: "is_object",
semantic_match: "V_01",
is_optional: true
}
]
},
instructions: {
vi: "Dùng để nói về khả năng hoặc sự cho phép.",
hint: "Modal + V1 (không chia)"
},
difficulty_score: 3,
category: "Modal Verbs",
tags: ["modal", "ability", "permission"],
mappings: [
{
book_id: "global-success-3",
grade: 3,
unit: 7,
lesson: 1
}
]
}
},
validation_checklist: [
"✓ grammar_code follows format: gram-XXX-identifier",
"✓ structure.formula is clear and educational",
"✓ pattern_logic has at least one slot",
"✓ Each slot has valid role from pattern_logic_roles",
"✓ use_form values match form_keys_reference",
"✓ semantic_filter types are consistent with vocab semantics",
"✓ Dependencies reference valid slot_ids",
"✓ Media story URLs are accessible",
"✓ difficulty_score is between 1-10",
"✓ At least one mapping exists for curriculum tracking"
],
common_mistakes: [
{
mistake: "Using invalid role in pattern_logic",
example: { role: "is_noun" },
fix: { role: "is_object" },
explanation: "Use is_object for nouns that can be sentence objects"
},
{
mistake: "Missing formula in structure",
example: { structure: { pattern_logic: [] } },
fix: { structure: { formula: "S + V + O", pattern_logic: [] } },
explanation: "formula is required for human readability"
},
{
mistake: "Invalid dependency reference",
example: { slot_id: "V_01", dependency: "INVALID_SLOT" },
fix: { slot_id: "V_01", dependency: "S_01" },
explanation: "dependency must reference an existing slot_id"
},
{
mistake: "Wrong form key for verb",
example: { role: "is_verb", use_form: "n_singular" },
fix: { role: "is_verb", use_form: "v_ing" },
explanation: "Verbs use v1, v_s_es, v_ing, v2, v3 forms"
},
{
mistake: "Semantic filter mismatch",
example: { role: "is_verb", semantic_filter: ["object"] },
fix: { role: "is_verb", semantic_filter: ["action"] },
explanation: "Verbs should filter by action/state, not object types"
}
],
ai_tips: {
efficiency: "Create grammar rules in order of difficulty (easy → hard)",
accuracy: "Always validate pattern_logic against actual vocab entries to ensure matches exist",
completeness: "Include mappings for curriculum tracking and media_stories for engagement",
testing: "After creating, test sentence generation with sample vocab to verify logic",
documentation: "Use clear formula and instructions for human teachers to understand"
}
};
res.json({
success: true,
data: guide
});
} catch (error) {
console.error('Error generating grammar guide:', error);
res.status(500).json({
success: false,
message: 'Failed to generate grammar guide',
error: error.message
});
}
};

View File

@@ -0,0 +1,858 @@
const { Lesson, Chapter, Subject, Vocab, Grammar, sequelize } = require('../models');
const { Op } = require('sequelize');
/**
* CREATE: Add new lesson
*/
exports.createLesson = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const {
chapter_id,
lesson_number,
lesson_title,
lesson_type = 'json_content',
lesson_description,
content_json,
content_url,
content_type,
lesson_content_type,
duration_minutes,
is_published = false,
is_free = false,
display_order = 0,
thumbnail_url
} = req.body;
// Validate required fields
if (!chapter_id || !lesson_number || !lesson_title) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Missing required fields: chapter_id, lesson_number, lesson_title'
});
}
// Validate content based on lesson_type
if (lesson_type === 'json_content' && !content_json) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'content_json is required when lesson_type is json_content'
});
}
if (lesson_type === 'url_content' && !content_url) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'content_url is required when lesson_type is url_content'
});
}
// Check if chapter exists
const chapter = await Chapter.findByPk(chapter_id);
if (!chapter) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Chapter not found'
});
}
// Create lesson
const lesson = await Lesson.create({
chapter_id,
lesson_number,
lesson_title,
lesson_type,
lesson_description,
content_json,
content_url,
content_type,
lesson_content_type,
duration_minutes,
is_published,
is_free,
display_order,
thumbnail_url
}, { transaction });
await transaction.commit();
res.status(201).json({
success: true,
message: 'Lesson created successfully',
data: lesson
});
} catch (error) {
await transaction.rollback();
console.error('Error creating lesson:', error);
res.status(500).json({
success: false,
message: 'Failed to create lesson',
error: error.message
});
}
};
/**
* READ: Get all lessons with pagination and filters
*/
exports.getAllLessons = async (req, res) => {
try {
const {
page = 1,
limit = 20,
chapter_id,
lesson_content_type,
lesson_type,
is_published,
is_free,
search
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(limit);
const where = {};
if (chapter_id) where.chapter_id = chapter_id;
if (lesson_content_type) where.lesson_content_type = lesson_content_type;
if (lesson_type) where.lesson_type = lesson_type;
if (is_published !== undefined) where.is_published = is_published === 'true';
if (is_free !== undefined) where.is_free = is_free === 'true';
if (search) {
where[Op.or] = [
{ lesson_title: { [Op.like]: `%${search}%` } },
{ lesson_description: { [Op.like]: `%${search}%` } }
];
}
const { count, rows } = await Lesson.findAndCountAll({
where,
include: [
{
model: Chapter,
as: 'chapter',
attributes: ['id', 'chapter_title', 'chapter_number'],
include: [
{
model: Subject,
as: 'subject',
attributes: ['id', 'subject_name', 'subject_code']
}
]
}
],
limit: parseInt(limit),
offset,
order: [['display_order', 'ASC'], ['lesson_number', 'ASC']]
});
res.json({
success: true,
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching lessons:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch lessons',
error: error.message
});
}
};
/**
* READ: Get lesson by ID
*/
exports.getLessonById = async (req, res) => {
try {
const { id } = req.params;
const lesson = await Lesson.findByPk(id, {
include: [
{
model: Chapter,
as: 'chapter',
include: [
{
model: Subject,
as: 'subject'
}
]
}
]
});
if (!lesson) {
return res.status(404).json({
success: false,
message: 'Lesson not found'
});
}
res.json({
success: true,
data: lesson
});
} catch (error) {
console.error('Error fetching lesson:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch lesson',
error: error.message
});
}
};
/**
* UPDATE: Update lesson
*/
exports.updateLesson = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { id } = req.params;
const updateData = req.body;
const lesson = await Lesson.findByPk(id);
if (!lesson) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Lesson not found'
});
}
await lesson.update(updateData, { transaction });
await transaction.commit();
res.json({
success: true,
message: 'Lesson updated successfully',
data: lesson
});
} catch (error) {
await transaction.rollback();
console.error('Error updating lesson:', error);
res.status(500).json({
success: false,
message: 'Failed to update lesson',
error: error.message
});
}
};
/**
* DELETE: Delete lesson
*/
exports.deleteLesson = async (req, res) => {
try {
const { id } = req.params;
const lesson = await Lesson.findByPk(id);
if (!lesson) {
return res.status(404).json({
success: false,
message: 'Lesson not found'
});
}
await lesson.destroy();
res.json({
success: true,
message: 'Lesson deleted successfully'
});
} catch (error) {
console.error('Error deleting lesson:', error);
res.status(500).json({
success: false,
message: 'Failed to delete lesson',
error: error.message
});
}
};
/**
* READ: Get lessons by chapter
*/
exports.getLessonsByChapter = async (req, res) => {
try {
const { chapter_id } = req.params;
const lessons = await Lesson.findAll({
where: { chapter_id },
order: [['display_order', 'ASC'], ['lesson_number', 'ASC']]
});
res.json({
success: true,
data: lessons,
count: lessons.length
});
} catch (error) {
console.error('Error fetching lessons by chapter:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch lessons by chapter',
error: error.message
});
}
};
/**
* GET GUIDE: Comprehensive guide for AI to create learning content
*/
exports.getLearningContentGuide = async (req, res) => {
try {
const guide = {
guide_version: "1.0.0",
last_updated: new Date().toISOString(),
hierarchy: {
description: "Learning content hierarchy: Subject → Chapter → Lesson",
structure: {
subject: "Top-level course/curriculum (e.g., 'English Grade 1')",
chapter: "Main topic within subject (e.g., 'Unit 1: My Family')",
lesson: "Individual learning activity (e.g., 'Lesson 1: Family Vocabulary')"
}
},
subject_structure: {
description: "Subject represents a complete course or curriculum",
required_fields: {
subject_code: {
type: "string(20)",
example: "ENG-G1",
description: "Unique code for subject"
},
subject_name: {
type: "string(100)",
example: "English Grade 1",
description: "Subject name"
}
},
optional_fields: {
subject_name_en: "English name",
description: "Subject description",
is_active: "Active status (default: true)",
is_premium: "Premium content flag",
is_training: "Training content flag",
is_public: "Public self-learning flag"
}
},
chapter_structure: {
description: "Chapter represents a major topic within a subject",
required_fields: {
subject_id: {
type: "UUID",
example: "550e8400-e29b-41d4-a716-446655440000",
description: "Parent subject ID"
},
chapter_number: {
type: "integer",
example: 1,
description: "Sequential chapter number"
},
chapter_title: {
type: "string(200)",
example: "Unit 1: My Family",
description: "Chapter title"
}
},
optional_fields: {
chapter_description: "Chapter description",
duration_minutes: "Estimated duration",
is_published: "Published status (default: false)",
display_order: "Custom display order"
}
},
lesson_structure: {
description: "Lesson represents individual learning activity",
required_fields: {
chapter_id: {
type: "UUID",
example: "550e8400-e29b-41d4-a716-446655440000",
description: "Parent chapter ID"
},
lesson_number: {
type: "integer",
example: 1,
description: "Sequential lesson number"
},
lesson_title: {
type: "string(200)",
example: "Family Vocabulary",
description: "Lesson title"
},
lesson_type: {
type: "enum",
options: ["json_content", "url_content"],
default: "json_content",
description: "Content type: JSON or URL"
}
},
optional_fields: {
lesson_description: "Lesson description",
lesson_content_type: "Content category: vocabulary, grammar, phonics, review, mixed",
content_json: "JSON content structure (see lesson_content_types below)",
content_url: "URL for external content",
content_type: "URL content type: video, audio, pdf, etc.",
duration_minutes: "Estimated duration",
is_published: "Published status",
is_free: "Free trial access",
display_order: "Custom display order",
thumbnail_url: "Lesson thumbnail"
}
},
lesson_content_types: {
vocabulary_lesson: {
description: "Vocabulary learning lesson with word list and exercises. System will lookup words in Vocab table when needed.",
structure: {
type: "vocabulary",
words: {
type: "array of strings OR array of objects",
description: "List of vocabulary words. System will search in Vocab table by base_word. Can be simple array of words or detailed objects.",
example_simple: ["mother", "father", "sister", "brother"],
example_detailed: [
{
word: "mother",
translation: "mẹ",
image: "https://cdn.sena.tech/vocab/mother.jpg",
audio: "https://cdn.sena.tech/audio/mother.mp3",
phonetic: "/ˈmʌð.ər/"
}
],
note: "Use simple array for words already in Vocab table. Use detailed objects for new/custom vocabulary."
},
exercises: {
type: "array of objects",
description: "Practice exercises",
example: [
{
type: "match",
question: "Match the words with pictures",
items: [
{ word: "mother", image: "..." },
{ word: "father", image: "..." }
]
},
{
type: "fill_blank",
question: "This is my ___",
answer: "mother",
options: ["mother", "father", "sister"]
}
]
}
},
example: {
type: "vocabulary",
words: ["mother", "father", "sister", "brother"],
exercises: [
{
type: "match",
question: "Match family members",
items: [
{ word: "mother", image: "https://cdn.sena.tech/img/mother.jpg" },
{ word: "father", image: "https://cdn.sena.tech/img/father.jpg" }
]
}
],
note: "System will lookup these words in Vocab table when rendering lesson"
}
},
grammar_lesson: {
description: "Grammar lesson with sentences and examples. System will lookup grammar patterns when needed.",
structure: {
type: "grammar",
grammar_points: {
type: "array of strings OR array of objects",
description: "Grammar points to teach. Can be grammar names or sentence patterns. System will search in Grammar table.",
example_simple: ["Present Simple", "Present Continuous"],
example_sentences: ["I eat an apple", "She eats an apple", "They are eating"],
note: "Use grammar names (e.g., 'Present Simple') or example sentences. System will find matching patterns in Grammar table."
},
sentences: {
type: "array of strings OR array of objects",
description: "Example sentences for this lesson",
example: ["I eat an apple", "She drinks water", "He plays football"]
},
examples: {
type: "array of objects",
description: "Example sentences",
example: [
{
sentence: "I eat an apple.",
translation: "Tôi ăn một quả táo.",
audio: "https://cdn.sena.tech/audio/example1.mp3"
}
]
},
exercises: {
type: "array of objects",
description: "Practice exercises",
example: [
{
type: "fill_blank",
question: "She ___ (eat) an apple every day.",
answer: "eats",
options: ["eat", "eats", "eating"]
},
{
type: "arrange_words",
question: "Arrange: apple / eat / I / an",
answer: "I eat an apple"
}
]
}
},
example: {
type: "grammar",
grammar_points: ["Present Simple"],
sentences: [
"I eat an apple.",
"She eats an apple.",
"They eat apples."
],
exercises: [
{
type: "fill_blank",
question: "She ___ an apple.",
answer: "eats",
options: ["eat", "eats", "eating"]
}
]
}
},
phonics_lesson: {
description: "Phonics/pronunciation lesson with IPA and sound practice",
structure: {
type: "phonics",
phonics_rules: {
type: "array of objects",
description: "Phonics rules with IPA and example words",
example: [
{
ipa: "/æ/",
sound_name: "short a",
description: "As in 'cat', 'hat'",
audio: "https://cdn.sena.tech/phonics/ae.mp3",
words: [
{ word: "cat", phonetic: "/kæt/", audio: "..." },
{ word: "hat", phonetic: "/hæt/", audio: "..." }
]
}
]
},
exercises: {
type: "array of objects",
description: "Pronunciation practice",
example: [
{
type: "listen_repeat",
question: "Listen and repeat",
audio: "https://cdn.sena.tech/audio/cat.mp3",
word: "cat"
},
{
type: "identify_sound",
question: "Which word has the /æ/ sound?",
options: ["cat", "cut", "cot"],
answer: "cat"
}
]
}
},
example: {
type: "phonics",
phonics_rules: [
{
ipa: "/æ/",
sound_name: "short a",
audio: "https://cdn.sena.tech/phonics/ae.mp3",
words: [
{ word: "cat", phonetic: "/kæt/" },
{ word: "hat", phonetic: "/hæt/" }
]
}
],
exercises: [
{
type: "listen_repeat",
audio: "https://cdn.sena.tech/audio/cat.mp3",
word: "cat"
}
]
}
},
review_lesson: {
description: "Review lesson combining vocabulary, grammar, and phonics",
structure: {
type: "review",
sections: {
type: "array of objects",
description: "Array containing vocabulary, grammar, and/or phonics sections",
example: [
{
section_type: "vocabulary",
title: "Family Vocabulary Review",
vocabulary_ids: [],
exercises: []
},
{
section_type: "grammar",
title: "Present Simple Review",
grammar_ids: [],
exercises: []
},
{
section_type: "phonics",
title: "Short A Sound Review",
phonics_rules: [],
exercises: []
}
]
},
overall_exercises: {
type: "array of objects",
description: "Mixed exercises covering all sections",
example: [
{
type: "mixed_quiz",
questions: [
{ type: "vocabulary", question: "...", answer: "..." },
{ type: "grammar", question: "...", answer: "..." }
]
}
]
}
},
example: {
type: "review",
sections: [
{
section_type: "vocabulary",
title: "Family Words",
words: ["mother", "father", "sister", "brother"]
},
{
section_type: "grammar",
title: "Present Simple",
grammar_points: ["Present Simple"],
sentences: ["I eat an apple", "She eats an apple"]
},
{
section_type: "phonics",
title: "Short A Sound",
phonics_rules: [
{
ipa: "/æ/",
words: ["cat", "hat", "bat"]
}
]
}
],
overall_exercises: [
{
type: "mixed_quiz",
questions: [
{ type: "vocabulary", question: "What is 'mẹ' in English?", answer: "mother" },
{ type: "grammar", question: "She ___ an apple", answer: "eats" }
]
}
],
note: "System will lookup words and grammar patterns from database when rendering"
}
}
},
exercise_types: {
description: "Common exercise types across all lesson types",
types: {
match: "Match items (words to images, words to translations)",
fill_blank: "Fill in the blank",
multiple_choice: "Multiple choice question",
arrange_words: "Arrange words to form sentence",
listen_repeat: "Listen and repeat (for phonics)",
identify_sound: "Identify the correct sound/word",
true_false: "True or false question",
mixed_quiz: "Mixed questions from different types"
}
},
api_workflow: {
description: "Step-by-step workflow to create learning content. No need to create Vocab/Grammar entries first - just use word lists and sentences directly.",
steps: [
{
step: 1,
action: "Create Subject",
endpoint: "POST /api/subjects",
example: {
subject_code: "ENG-G1",
subject_name: "English Grade 1"
}
},
{
step: 2,
action: "Create Chapter",
endpoint: "POST /api/chapters",
example: {
subject_id: "<subject_uuid>",
chapter_number: 1,
chapter_title: "Unit 1: My Family"
}
},
{
step: 3,
action: "Create Vocabulary Lesson (using word list)",
endpoint: "POST /api/learning-content/lessons",
example: {
chapter_id: "<chapter_uuid>",
lesson_number: 1,
lesson_title: "Family Vocabulary",
lesson_type: "json_content",
lesson_content_type: "vocabulary",
content_json: {
type: "vocabulary",
words: ["mother", "father", "sister", "brother"],
exercises: []
}
},
note: "System will search for these words in Vocab table when rendering lesson"
},
{
step: 4,
action: "Create Grammar Lesson (using sentences)",
endpoint: "POST /api/learning-content/lessons",
example: {
chapter_id: "<chapter_uuid>",
lesson_number: 2,
lesson_title: "Present Simple",
lesson_type: "json_content",
lesson_content_type: "grammar",
content_json: {
type: "grammar",
grammar_points: ["Present Simple"],
sentences: ["I eat an apple", "She eats an apple"],
exercises: []
}
},
note: "System will find matching grammar patterns from Grammar table"
},
{
step: 5,
action: "Create Review Lesson",
endpoint: "POST /api/learning-content/lessons",
example: {
chapter_id: "<chapter_uuid>",
lesson_number: 3,
lesson_title: "Unit Review",
lesson_type: "json_content",
lesson_content_type: "review",
content_json: {
type: "review",
sections: [
{ section_type: "vocabulary", words: ["mother", "father"] },
{ section_type: "grammar", sentences: ["I eat an apple"] }
],
overall_exercises: []
}
}
}
]
},
validation_checklist: [
"✓ Subject, Chapter, Lesson hierarchy is maintained",
"✓ lesson_type matches content (json_content has content_json, url_content has content_url)",
"✓ lesson_content_type is set correctly (vocabulary, grammar, phonics, review)",
"✓ content_json structure matches lesson_content_type",
"✓ For vocabulary lessons: words array is provided (system will lookup in Vocab table)",
"✓ For grammar lessons: sentences or grammar_points are provided (system will lookup in Grammar table)",
"✓ Exercise types are valid",
"✓ URLs are accessible (audio, images, videos)",
"✓ Lesson numbers are sequential within chapter"
],
common_mistakes: [
{
mistake: "Missing parent references",
example: { lesson_title: "Vocab" },
fix: { chapter_id: "<uuid>", lesson_title: "Vocab" },
explanation: "Always provide chapter_id for lessons"
},
{
mistake: "Wrong content type",
example: { lesson_type: "json_content", content_url: "..." },
fix: { lesson_type: "url_content", content_url: "..." },
explanation: "lesson_type must match content field"
},
{
mistake: "Using old vocabulary_ids format",
example: { vocabulary_ids: ["uuid1", "uuid2"] },
fix: { words: ["mother", "father", "sister"] },
explanation: "Use words array instead of vocabulary_ids. System will lookup words in Vocab table."
},
{
mistake: "Using old grammar_ids format",
example: { grammar_ids: ["uuid1"] },
fix: { grammar_points: ["Present Simple"], sentences: ["I eat an apple"] },
explanation: "Use grammar_points or sentences instead of grammar_ids. System will find patterns in Grammar table."
},
{
mistake: "Missing content_json type",
example: { content_json: { exercises:[]} },
fix: { content_json: { type: "vocabulary", exercises: [] } },
explanation: "content_json must have type field"
}
],
ai_tips: {
planning: "Plan subject → chapter → lesson hierarchy before creating",
word_lists: "Use simple word arrays like ['mother', 'father']. System will automatically lookup in Vocab table when rendering.",
sentences: "Use sentence arrays like ['I eat an apple', 'She eats an apple']. System will find matching grammar patterns.",
consistency: "Keep lesson_content_type and content_json.type consistent",
exercises: "Include diverse exercise types for better engagement",
multimedia: "Provide audio and images for better learning experience",
no_ids_needed: "No need to create Vocab/Grammar entries first - just use word lists and sentences directly!"
}
};
res.json({
success: true,
data: guide
});
} catch (error) {
console.error('Error generating learning content guide:', error);
res.status(500).json({
success: false,
message: 'Failed to generate learning content guide',
error: error.message
});
}
};

View File

@@ -0,0 +1,642 @@
const { Story, sequelize } = require('../models');
const { Op } = require('sequelize');
/**
* CREATE: Add new story
*/
exports.createStory = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const {
name,
logo,
vocabulary = [],
context = [],
grade = [],
tag = []
} = req.body;
// Validate required fields
if (!name) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Missing required field: name'
});
}
// Create story
const story = await Story.create({
name,
logo,
vocabulary,
context,
grade,
tag
}, { transaction });
await transaction.commit();
res.status(201).json({
success: true,
message: 'Story created successfully',
data: story
});
} catch (error) {
await transaction.rollback();
console.error('Error creating story:', error);
res.status(500).json({
success: false,
message: 'Failed to create story',
error: error.message
});
}
};
/**
* READ: Get all stories with pagination and filters
*/
exports.getAllStories = async (req, res) => {
try {
const {
page = 1,
limit = 20,
search,
grade_filter,
tag_filter,
sort_by = 'created_at',
sort_order = 'DESC'
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(limit);
const where = {};
// Search filter
if (search) {
where.name = { [Op.like]: `%${search}%` };
}
// Grade filter
if (grade_filter) {
where.grade = {
[Op.contains]: [grade_filter]
};
}
// Tag filter
if (tag_filter) {
where.tag = {
[Op.contains]: [tag_filter]
};
}
const { count, rows } = await Story.findAndCountAll({
where,
limit: parseInt(limit),
offset,
order: [[sort_by, sort_order.toUpperCase()]],
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at', 'updated_at']
});
res.json({
success: true,
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching stories:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch stories',
error: error.message
});
}
};
/**
* READ: Get story by ID
*/
exports.getStoryById = async (req, res) => {
try {
const { id } = req.params;
const story = await Story.findByPk(id);
if (!story) {
return res.status(404).json({
success: false,
message: 'Story not found'
});
}
res.json({
success: true,
data: story
});
} catch (error) {
console.error('Error fetching story:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch story',
error: error.message
});
}
};
/**
* UPDATE: Update story
*/
exports.updateStory = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { id } = req.params;
const {
name,
logo,
vocabulary,
context,
grade,
tag
} = req.body;
const story = await Story.findByPk(id);
if (!story) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Story not found'
});
}
// Update fields
const updateData = {};
if (name !== undefined) updateData.name = name;
if (logo !== undefined) updateData.logo = logo;
if (vocabulary !== undefined) updateData.vocabulary = vocabulary;
if (context !== undefined) updateData.context = context;
if (grade !== undefined) updateData.grade = grade;
if (tag !== undefined) updateData.tag = tag;
await story.update(updateData, { transaction });
await transaction.commit();
res.json({
success: true,
message: 'Story updated successfully',
data: story
});
} catch (error) {
await transaction.rollback();
console.error('Error updating story:', error);
res.status(500).json({
success: false,
message: 'Failed to update story',
error: error.message
});
}
};
/**
* DELETE: Delete story
*/
exports.deleteStory = async (req, res) => {
try {
const { id } = req.params;
const story = await Story.findByPk(id);
if (!story) {
return res.status(404).json({
success: false,
message: 'Story not found'
});
}
await story.destroy();
res.json({
success: true,
message: 'Story deleted successfully'
});
} catch (error) {
console.error('Error deleting story:', error);
res.status(500).json({
success: false,
message: 'Failed to delete story',
error: error.message
});
}
};
/**
* READ: Get stories by grade
*/
exports.getStoriesByGrade = async (req, res) => {
try {
const { grade } = req.query;
if (!grade) {
return res.status(400).json({
success: false,
message: 'Grade parameter is required'
});
}
const stories = await Story.findAll({
where: {
grade: {
[Op.contains]: [grade]
}
},
order: [['created_at', 'DESC']],
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at']
});
res.json({
success: true,
data: stories,
count: stories.length
});
} catch (error) {
console.error('Error fetching stories by grade:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch stories by grade',
error: error.message
});
}
};
/**
* READ: Get stories by tag
*/
exports.getStoriesByTag = async (req, res) => {
try {
const { tag } = req.query;
if (!tag) {
return res.status(400).json({
success: false,
message: 'Tag parameter is required'
});
}
const stories = await Story.findAll({
where: {
tag: {
[Op.contains]: [tag]
}
},
order: [['created_at', 'DESC']],
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at']
});
res.json({
success: true,
data: stories,
count: stories.length
});
} catch (error) {
console.error('Error fetching stories by tag:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch stories by tag',
error: error.message
});
}
};
/**
* READ: Get story statistics
*/
exports.getStoryStats = async (req, res) => {
try {
const totalStories = await Story.count();
// Get all stories to analyze grades and tags
const allStories = await Story.findAll({
attributes: ['grade', 'tag']
});
// Count by grade
const gradeStats = {};
allStories.forEach(story => {
if (story.grade && Array.isArray(story.grade)) {
story.grade.forEach(g => {
gradeStats[g] = (gradeStats[g] || 0) + 1;
});
}
});
// Count by tag
const tagStats = {};
allStories.forEach(story => {
if (story.tag && Array.isArray(story.tag)) {
story.tag.forEach(t => {
tagStats[t] = (tagStats[t] || 0) + 1;
});
}
});
res.json({
success: true,
data: {
total: totalStories,
by_grade: Object.entries(gradeStats).map(([grade, count]) => ({ grade, count })),
by_tag: Object.entries(tagStats).map(([tag, count]) => ({ tag, count }))
}
});
} catch (error) {
console.error('Error fetching story stats:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch story statistics',
error: error.message
});
}
};
/**
* GET GUIDE: Comprehensive guide for AI to create stories
*/
exports.getStoryGuide = async (req, res) => {
try {
const guide = {
guide_version: "1.0.0",
last_updated: new Date().toISOString(),
data_structure: {
required_fields: {
name: {
type: "string(255)",
example: "The Greedy Cat",
description: "Story title/name"
}
},
optional_fields: {
logo: {
type: "text (URL)",
example: "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
description: "URL to story logo/thumbnail image"
},
vocabulary: {
type: "JSON array",
example: ["cat", "eat", "apple", "happy"],
description: "Array of vocabulary words used in the story"
},
context: {
type: "JSON array of objects",
description: "Array of story context objects with images, text, audio data",
item_structure: {
image: "URL to context image",
text: "Story text for this context",
audio: "URL to audio narration",
order: "Sequence number (integer)"
},
example: [
{
image: "https://cdn.sena.tech/story/scene1.jpg",
text: "Once upon a time, there was a greedy cat.",
audio: "https://cdn.sena.tech/audio/scene1.mp3",
order: 1
},
{
image: "https://cdn.sena.tech/story/scene2.jpg",
text: "The cat loved eating apples.",
audio: "https://cdn.sena.tech/audio/scene2.mp3",
order: 2
}
]
},
grade: {
type: "JSON array",
example: ["Grade 1", "Grade 2", "Grade 3"],
description: "Array of grade levels this story is suitable for"
},
tag: {
type: "JSON array",
example: ["adventure", "friendship", "animals", "food"],
description: "Array of tags for categorization and filtering"
}
}
},
context_structure: {
description: "Each context object represents a scene/page in the story",
required_properties: {
text: "Story text content (required)",
order: "Sequence number starting from 1 (required)"
},
optional_properties: {
image: "URL to scene image",
audio: "URL to audio narration"
},
best_practices: [
"Keep text concise and age-appropriate",
"Use order numbers sequentially (1, 2, 3, ...)",
"Provide audio for better engagement",
"Images should be high quality and relevant"
]
},
vocabulary_guidelines: {
description: "Link vocabulary words to existing vocab entries",
format: "Array of strings",
tips: [
"Use vocab_code from Vocab table for better tracking",
"Include only words that appear in the story",
"Order by frequency or importance",
"Can reference vocab IDs or codes"
],
example: ["vocab-001-cat", "vocab-002-eat", "vocab-015-apple"]
},
grade_levels: {
description: "Supported grade levels",
options: [
"Pre-K",
"Kindergarten",
"Grade 1",
"Grade 2",
"Grade 3",
"Grade 4",
"Grade 5",
"Grade 6"
],
tips: [
"Can assign multiple grades",
"Consider vocabulary difficulty",
"Match with curriculum standards"
]
},
tag_categories: {
description: "Common tag categories for story classification",
categories: {
themes: ["adventure", "friendship", "family", "courage", "honesty", "kindness"],
subjects: ["animals", "nature", "food", "school", "home", "travel"],
skills: ["reading", "listening", "vocabulary", "grammar", "phonics"],
emotions: ["happy", "sad", "excited", "scared", "surprised"],
genres: ["fiction", "non-fiction", "fantasy", "realistic", "fable"]
},
tips: [
"Use 3-7 tags per story",
"Mix different category types",
"Keep tags consistent across stories",
"Use lowercase for consistency"
]
},
examples: {
complete_story: {
name: "The Greedy Cat",
logo: "https://cdn.sena.tech/thumbs/greedy-cat.jpg",
vocabulary: ["cat", "eat", "apple", "happy", "greedy"],
context: [
{
image: "https://cdn.sena.tech/story/gc-scene1.jpg",
text: "Once upon a time, there was a greedy cat.",
audio: "https://cdn.sena.tech/audio/gc-scene1.mp3",
order: 1
},
{
image: "https://cdn.sena.tech/story/gc-scene2.jpg",
text: "The cat loved eating apples every day.",
audio: "https://cdn.sena.tech/audio/gc-scene2.mp3",
order: 2
},
{
image: "https://cdn.sena.tech/story/gc-scene3.jpg",
text: "One day, the cat ate too many apples and felt sick.",
audio: "https://cdn.sena.tech/audio/gc-scene3.mp3",
order: 3
},
{
image: "https://cdn.sena.tech/story/gc-scene4.jpg",
text: "The cat learned to eat just enough and was happy again.",
audio: "https://cdn.sena.tech/audio/gc-scene4.mp3",
order: 4
}
],
grade: ["Grade 1", "Grade 2"],
tag: ["animals", "food", "lesson", "health", "fiction"]
},
minimal_story: {
name: "My Pet Dog",
context: [
{
text: "I have a pet dog.",
order: 1
},
{
text: "My dog is brown.",
order: 2
},
{
text: "I love my dog.",
order: 3
}
]
}
},
validation_checklist: [
"✓ name is provided and descriptive",
"✓ context array has at least 1 scene",
"✓ Each context has text and order",
"✓ order numbers are sequential (1, 2, 3...)",
"✓ vocabulary words match story content",
"✓ grade levels are appropriate for content",
"✓ tags are relevant and descriptive",
"✓ URLs are accessible (logo, images, audio)",
"✓ Text is age-appropriate",
"✓ Story has clear beginning, middle, end"
],
common_mistakes: [
{
mistake: "Missing context order",
example: { context: [{ text: "Some text" }] },
fix: { context: [{ text: "Some text", order: 1 }] },
explanation: "Every context must have an order number"
},
{
mistake: "Non-sequential order numbers",
example: { context: [{ order: 1 }, { order: 3 }, { order: 2 }] },
fix: { context: [{ order: 1 }, { order: 2 }, { order: 3 }] },
explanation: "Order should be sequential for proper story flow"
},
{
mistake: "Invalid grade format",
example: { grade: "Grade 1" },
fix: { grade: ["Grade 1"] },
explanation: "grade must be an array"
},
{
mistake: "Mismatched vocabulary",
example: { name: "The Cat", vocabulary: ["dog", "bird"] },
fix: { name: "The Cat", vocabulary: ["cat"] },
explanation: "Vocabulary should match words in the story"
},
{
mistake: "Missing story name",
example: { context: [/* context items */] },
fix: { name: "My Story", context: [/* context items */] },
explanation: "name is required"
}
],
ai_tips: {
content_creation: "Generate age-appropriate, engaging stories with clear moral lessons",
vocabulary_integration: "Reference existing vocabulary entries from Vocab table",
multimedia: "Always provide audio URLs for better learning engagement",
sequencing: "Ensure context order is logical and sequential",
testing: "Test story flow by reading context in order",
consistency: "Use consistent naming conventions for URLs and vocabulary"
},
api_usage: {
create: "POST /api/stories",
get_all: "GET /api/stories?page=1&limit=20&grade_filter=Grade 1&tag_filter=animals",
get_one: "GET /api/stories/:id",
update: "PUT /api/stories/:id",
delete: "DELETE /api/stories/:id",
by_grade: "GET /api/stories/grade?grade=Grade 1",
by_tag: "GET /api/stories/tag?tag=adventure",
stats: "GET /api/stories/stats",
guide: "GET /api/stories/guide"
}
};
res.json({
success: true,
data: guide
});
} catch (error) {
console.error('Error generating story guide:', error);
res.status(500).json({
success: false,
message: 'Failed to generate story guide',
error: error.message
});
}
};

File diff suppressed because it is too large Load Diff

108
data/moveup/g1/unit4.json Normal file
View File

@@ -0,0 +1,108 @@
[
{
"chapter_id": "uuid_chapter_4",
"lesson_number": 1,
"lesson_title": "Lesson 1: Animals",
"lesson_type": "json_content",
"lesson_description": "Identify 3 animals",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": ["identify 3 animals"],
"vocabulary": ["monkey", "lion", "elephant"]
}
},
{
"chapter_id": "uuid_chapter_4",
"lesson_number": 2,
"lesson_title": "Lesson 2: Animal Questions",
"lesson_type": "json_content",
"lesson_description": "Ask and answer questions about animals",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"ask question: 'Are they elephants?'",
"answer: 'Yes, they are. / No, they aren't.'"
],
"grammar": "Are they elephants? Yes, they are. / No, they aren't."
}
},
{
"chapter_id": "uuid_chapter_4",
"lesson_number": 3,
"lesson_title": "Lesson 3: Letters J & K",
"lesson_type": "json_content",
"lesson_description": "Recognize letters J, K and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize the upper- and lowercase forms of the letters J, K",
"associate them with the sounds /dʒ/ and /k/"
],
"letters": ["J", "K"],
"sounds": ["/dʒ/", "/k/"],
"vocabulary": ["jellyfish", "juice", "kite", "kangaroo"]
}
},
{
"chapter_id": "uuid_chapter_4",
"lesson_number": 4,
"lesson_title": "Lesson 4: Letters L & M",
"lesson_type": "json_content",
"lesson_description": "Recognize letters L, M and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize the upper- and lowercase forms of the letters L, M",
"associate them with the sounds /l/ and /m/"
],
"letters": ["L", "M"],
"sounds": ["/l/", "/m/"],
"vocabulary": ["lion", "lollipop", "man", "mango", "love", "music"]
}
},
{
"chapter_id": "uuid_chapter_4",
"lesson_number": 5,
"lesson_title": "Lesson 5: Review Animals",
"lesson_type": "json_content",
"lesson_description": "Review animals and questions",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review animals",
"review question and answer: What are they? They're...",
"review question and answer: Are they...? Yes/No"
]
}
},
{
"chapter_id": "uuid_chapter_4",
"lesson_number": 6,
"lesson_title": "Lesson 6: Challenge",
"lesson_type": "json_content",
"lesson_description": "Review unit 4",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": ["review the animals", "review unit 4"]
}
},
{
"chapter_id": "uuid_chapter_4",
"lesson_number": 7,
"lesson_title": "Lesson 7: My Own Work",
"lesson_type": "url_content",
"lesson_description": "Review animals, make a paper lion",
"content_url": "/activities/paper-lion-tutorial",
"content_type": "video_tutorial",
"content_json": {
"type": "activity",
"learning_objectives": ["review animals", "make a paper lion craft"]
}
}
]

116
data/moveup/g1/unit5.json Normal file
View File

@@ -0,0 +1,116 @@
[
{
"chapter_id": "uuid_chapter_5",
"lesson_number": 1,
"lesson_title": "Lesson 1: Body Parts",
"lesson_type": "json_content",
"lesson_description": "Identify body parts and use in dialogue",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify body parts",
"use the words in the content of a dialogue"
],
"vocabulary": ["arms", "nose", "face", "legs", "fingers", "hands"]
}
},
{
"chapter_id": "uuid_chapter_5",
"lesson_number": 2,
"lesson_title": "Lesson 2: Body Parts Questions",
"lesson_type": "json_content",
"lesson_description": "Ask and answer about body parts",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"say sentences with these",
"complete sentences with these",
"recognize plural forms of nouns"
],
"grammar": "Are these your legs? Yes, they are./ No, they aren't"
}
},
{
"chapter_id": "uuid_chapter_5",
"lesson_number": 3,
"lesson_title": "Lesson 3: Letters A, B, C, D",
"lesson_type": "json_content",
"lesson_description": "Recognize letters A, B, C, D and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize and say the names of the letters a, b, c, d",
"review the upper and lower forms of a, b, c, d",
"pronounce /b/ and /d/ at the ends of words"
],
"letters": ["A", "B", "C", "D"],
"sounds": ["/æ/", "/b/", "/k/", "/d/"],
"vocabulary": ["ant", "bus", "cake", "doll"]
}
},
{
"chapter_id": "uuid_chapter_5",
"lesson_number": 4,
"lesson_title": "Lesson 4: Letters E, F, G, H",
"lesson_type": "json_content",
"lesson_description": "Recognize letters E, F, G, H and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"review the upper- and lowercase forms of the letter e, f, g, and h",
"associate them with their corresponding sounds",
"pronounce the sound /f/ and /g/ at the ends of words",
"recognize the names of the letters Ee, Ff, Gg, and H"
],
"letters": ["E", "F", "G", "H"],
"sounds": ["/e/", "/f/", "/g/", "/h/"],
"vocabulary": ["eraser", "fan", "gift", "hamster", "shelf", "bag"]
}
},
{
"chapter_id": "uuid_chapter_5",
"lesson_number": 5,
"lesson_title": "Lesson 5: Review Body Parts",
"lesson_type": "json_content",
"lesson_description": "Review body parts and unit 5",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": ["review the body parts", "review unit 5"]
}
},
{
"chapter_id": "uuid_chapter_5",
"lesson_number": 6,
"lesson_title": "Lesson 6: Challenge",
"lesson_type": "json_content",
"lesson_description": "Review unit 5 and check understanding",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review unit 5",
"check their understanding about body parts"
]
}
},
{
"chapter_id": "uuid_chapter_5",
"lesson_number": 7,
"lesson_title": "Lesson 7: My Own Work",
"lesson_type": "json_content",
"lesson_description": "Review body parts, draw and write parts of body",
"lesson_content_type": "review",
"content_json": {
"type": "activity",
"learning_objectives": [
"review body parts",
"draw and write parts of body"
]
}
}
]

106
data/moveup/g1/unit6.json Normal file
View File

@@ -0,0 +1,106 @@
[
{
"chapter_id": "uuid_chapter_6",
"lesson_number": 1,
"lesson_title": "Lesson 1: Food and Drinks",
"lesson_type": "json_content",
"lesson_description": "Identify some food and drink",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": ["identify some food and drink"],
"vocabulary": ["salad", "milk", "pizza"]
}
},
{
"chapter_id": "uuid_chapter_6",
"lesson_number": 2,
"lesson_title": "Lesson 2: Food Questions",
"lesson_type": "json_content",
"lesson_description": "Ask questions with HAVE, recognize A vs AN",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"make Yes/No questions with HAVE",
"recognize the difference between A and AN"
],
"grammar": "Do you have an apple? Yes, I do./ No, I don't."
}
},
{
"chapter_id": "uuid_chapter_6",
"lesson_number": 3,
"lesson_title": "Lesson 3: Letters I, J, K, L, M",
"lesson_type": "json_content",
"lesson_description": "Recognize letters I, J, K, L, M and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize and say the names of the letters I, j, k, l, m",
"review the upper and lower forms of the letters",
"associate them with their corresponding sounds",
"pronounce the sounds /ɪ/, /ʤ/, /k/, /l/, /m/ at the beginning of words",
"pronounce the sounds /k/, /l/, /m/ at the end of words"
],
"letters": ["I", "J", "K", "L", "M"],
"sounds": ["/ɪ/", "/ʤ/", "/k/", "/l/", "/m/"],
"vocabulary": ["insect", "jellyfish", "kangaroo", "lollipop", "man"]
}
},
{
"chapter_id": "uuid_chapter_6",
"lesson_number": 4,
"lesson_title": "Lesson 4: Review Numbers and Colors",
"lesson_type": "json_content",
"lesson_description": "Review numbers 1-10 and colors",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": ["review numbers 1-10", "review colors"]
}
},
{
"chapter_id": "uuid_chapter_6",
"lesson_number": 5,
"lesson_title": "Lesson 5: Review Food and Drinks",
"lesson_type": "json_content",
"lesson_description": "Review numbers 1-10 and colors",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": ["review numbers 1-10", "review colors"]
}
},
{
"chapter_id": "uuid_chapter_6",
"lesson_number": 6,
"lesson_title": "Lesson 6: Challenge",
"lesson_type": "json_content",
"lesson_description": "Review unit 6 and check understanding",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review unit 6",
"check their understanding about food and drinks"
]
}
},
{
"chapter_id": "uuid_chapter_6",
"lesson_number": 7,
"lesson_title": "Lesson 7: Fun Time",
"lesson_type": "json_content",
"lesson_description": "Review food and drinks, draw and write",
"lesson_content_type": "review",
"content_json": {
"type": "activity",
"learning_objectives": [
"review food and drinks",
"draw and write food and drinks"
]
}
}
]

105
data/moveup/g2/unit4.json Normal file
View File

@@ -0,0 +1,105 @@
[
{
"chapter_id": "uuid_chapter_4_grade2",
"lesson_number": 1,
"lesson_title": "Lesson 1: Transportation Words",
"lesson_type": "json_content",
"lesson_description": "Review transportation words and identify 4 more",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"review transportation words",
"identify 4 more transportation words"
],
"vocabulary": ["ambulance", "tricycle", "subway", "helicopter"]
}
},
{
"chapter_id": "uuid_chapter_4_grade2",
"lesson_number": 2,
"lesson_title": "Lesson 2: Describing Transportation",
"lesson_type": "json_content",
"lesson_description": "Describe transportation and improve integrated skills",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"review how to describe their means of transportation for going to school",
"describe how they move",
"improve integrated skills"
],
"grammar": "I go to the park with my family by tricycle."
}
},
{
"chapter_id": "uuid_chapter_4_grade2",
"lesson_number": 3,
"lesson_title": "Lesson 3: Letters W & X",
"lesson_type": "json_content",
"lesson_description": "Recognize letters W, X and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize and trace the uppercase and lowercase forms of the letter W and X",
"pronounce the sound /w/ and /s/ on its own and at the beginning of words",
"be familiar with the letter name for W and X"
],
"letters": ["W", "X"],
"sounds": ["/w/", "/s/"],
"vocabulary": ["water", "watch", "fox", "six"]
}
},
{
"chapter_id": "uuid_chapter_4_grade2",
"lesson_number": 4,
"lesson_title": "Lesson 4: Letters Y & Z",
"lesson_type": "json_content",
"lesson_description": "Recognize letters Y, Z and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize and trace the uppercase and lowercase forms of the letter Y and Z",
"pronounce these sounds on its own and at the beginning of words",
"be familiar with the letter name for Y and Z"
],
"letters": ["Y", "Z"],
"sounds": ["/j/", "/z/"],
"vocabulary": ["yacht", "yak", "zoo", "zipper"]
}
},
{
"chapter_id": "uuid_chapter_4_grade2",
"lesson_number": 5,
"lesson_title": "Lesson 5: Review Unit 4",
"lesson_type": "json_content",
"lesson_description": "Review transportation words and letters",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review Unit 4",
"review transportation words",
"review letters and sounds Ww, Xx, Yy, Zz"
]
}
},
{
"chapter_id": "uuid_chapter_4_grade2",
"lesson_number": 6,
"lesson_title": "Lesson 6: Challenge",
"lesson_type": "json_content",
"lesson_description": "Review Unit 4 content",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review Unit 4",
"review transportation words",
"review letters and sounds Ww, Xx, Yy, Zz"
]
}
}
]

100
data/moveup/g2/unit5.json Normal file
View File

@@ -0,0 +1,100 @@
[
{
"chapter_id": "uuid_chapter_5_grade2",
"lesson_number": 1,
"lesson_title": "Lesson 1: Things in the Park",
"lesson_type": "json_content",
"lesson_description": "Review park things and identify 4 more",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"review things in the park words",
"identify 4 more things in the park words"
],
"vocabulary": ["picnic table", "bench", "swing", "bin"]
}
},
{
"chapter_id": "uuid_chapter_5_grade2",
"lesson_number": 2,
"lesson_title": "Lesson 2: Park Questions",
"lesson_type": "json_content",
"lesson_description": "Ask and answer questions about locations",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"review how to ask and answer Where's the...? It's on the...",
"ask and answer Yes/No questions",
"improve integrated skills"
],
"grammar": "Is the Frisbee in the tree? Yes, it is/ No, it isn't."
}
},
{
"chapter_id": "uuid_chapter_5_grade2",
"lesson_number": 3,
"lesson_title": "Lesson 3: Letters N to U",
"lesson_type": "json_content",
"lesson_description": "Recognize letters N to U and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize and trace the uppercase and lowercase forms of the letters N to U",
"pronounce the sounds on their own and at the beginning of words",
"be familiar with the letter name for N, O, P, Q, R, S, T and U"
],
"letters": ["N", "O", "P", "Q", "R", "S", "T", "U"],
"sounds": ["/n/", "/ɒ/", "/p/", "/kw/", "/r/", "/s/", "/t/", "/ʌ/"],
"vocabulary": ["nest", "ostrich", "panda", "quick", "river", "sun", "ten", "umpire"]
}
},
{
"chapter_id": "uuid_chapter_5_grade2",
"lesson_number": 4,
"lesson_title": "Lesson 4: Final Sounds",
"lesson_type": "json_content",
"lesson_description": "Recognize final sounds /n/, /p/, /r/, /s/, /t/",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": ["recognize the final sounds /n/, /p/, /r/, /s/, and /t/"],
"sounds": ["/n/", "/p/", "/r/", "/s/", "/t/"],
"vocabulary": ["fan", "cup", "bear", "grass", "bat"]
}
},
{
"chapter_id": "uuid_chapter_5_grade2",
"lesson_number": 5,
"lesson_title": "Lesson 5: Review Unit 5",
"lesson_type": "json_content",
"lesson_description": "Review park things and letters",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review Unit 5",
"review thing words in the park",
"review letters and sounds"
]
}
},
{
"chapter_id": "uuid_chapter_5_grade2",
"lesson_number": 6,
"lesson_title": "Lesson 6: Challenge",
"lesson_type": "json_content",
"lesson_description": "Review Unit 5 content",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review Unit 5",
"review transportation words",
"review letters and sounds Nn Uu"
]
}
}
]

101
data/moveup/g2/unit6.json Normal file
View File

@@ -0,0 +1,101 @@
[
{
"chapter_id": "uuid_chapter_6_grade2",
"lesson_number": 1,
"lesson_title": "Lesson 1: Places in a Home",
"lesson_type": "json_content",
"lesson_description": "Review home places and identify 4 more",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"review different places in a home",
"identify 4 more different places in a home"
],
"vocabulary": ["laundry room", "basement", "garage", "balcony"]
}
},
{
"chapter_id": "uuid_chapter_6_grade2",
"lesson_number": 2,
"lesson_title": "Lesson 2: Home Questions and Prepositions",
"lesson_type": "json_content",
"lesson_description": "Ask about locations and identify prepositions",
"lesson_content_type": "mixed",
"content_json": {
"type": "mixed",
"learning_objectives": [
"review how to ask and answer Where's the...? It's on the...",
"identify preposition of places",
"improve integrated skills"
],
"vocabulary": ["in front of", "behind", "next to", "between"],
"grammar": "Where is the juice? It's on the table.\nIs the bat between the duck and the cat? Yes, it is./ No, it isn't."
}
},
{
"chapter_id": "uuid_chapter_6_grade2",
"lesson_number": 3,
"lesson_title": "Lesson 3: Letters V, W, Y, Z",
"lesson_type": "json_content",
"lesson_description": "Recognize letters V, W, Y, Z and their sounds",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": [
"recognize and trace the uppercase and lowercase forms of the letters Vv, Ww, Yy and Zz",
"pronounce the sounds on their own and at the beginning of words",
"be familiar with the letter name for V, W, Y and Z"
],
"letters": ["V", "W", "Y", "Z"],
"sounds": ["/v/", "/w/", "/j/", "/z/"],
"vocabulary": ["violin", "wand", "yoghurt", "zebrafish"]
}
},
{
"chapter_id": "uuid_chapter_6_grade2",
"lesson_number": 4,
"lesson_title": "Lesson 4: Final Sounds /ks/, /z/",
"lesson_type": "json_content",
"lesson_description": "Recognize final sounds /ks/ and /z/",
"lesson_content_type": "phonics",
"content_json": {
"type": "phonics",
"learning_objectives": ["recognize the final sounds /ks/, /z/"],
"sounds": ["/ks/", "/z/"],
"vocabulary": ["mailbox", "sax", "quiz", "topaz"]
}
},
{
"chapter_id": "uuid_chapter_6_grade2",
"lesson_number": 5,
"lesson_title": "Lesson 5: Review Unit 6",
"lesson_type": "json_content",
"lesson_description": "Review home places and letters",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review Unit 6",
"review different places in a home",
"review letters and sounds"
]
}
},
{
"chapter_id": "uuid_chapter_6_grade2",
"lesson_number": 6,
"lesson_title": "Lesson 6: Challenge",
"lesson_type": "json_content",
"lesson_description": "Review Unit 6 content",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review Unit 6",
"review different places in a home",
"review letters and sounds Vv Zz"
]
}
}
]

View File

@@ -0,0 +1,39 @@
[
{
"chapter_id": "uuid_chapter_10_grade3",
"lesson_number": 1,
"lesson_title": "Lesson 1: Outdoor Activities",
"lesson_type": "json_content",
"lesson_description": "Identify phrases about outdoor activities and understand a story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify the phrases about outdoor activities",
"understand a story"
],
"vocabulary": ["play on the swing", "ride a bike", "have a picnic", "fly a kite"]
}
},
{
"chapter_id": "uuid_chapter_10_grade3",
"lesson_number": 2,
"lesson_title": "Lesson 2: Asking for Permission",
"lesson_type": "json_content",
"lesson_description": "Ask for and give permission",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": ["ask for and give permission"],
"grammar": "Can I play on the swing? Yes, you can.\nCan I ride a bike? No, you can't."
}
},
{
"chapter_id": "uuid_chapter_10_grade3",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Exam preparation for Unit 10",
"lesson_content_type": "review"
}
]

View File

@@ -0,0 +1,41 @@
[
{
"chapter_id": "uuid_chapter_11_grade3",
"lesson_number": 1,
"lesson_title": "Lesson 1: Things in the House",
"lesson_type": "json_content",
"lesson_description": "Identify things in the house and understand a story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify things in the house",
"understand a story"
],
"vocabulary": ["clock", "mirror", "picture", "lamp", "cupboard"]
}
},
{
"chapter_id": "uuid_chapter_11_grade3",
"lesson_number": 2,
"lesson_title": "Lesson 2: Questions About Quantity",
"lesson_type": "json_content",
"lesson_description": "Ask and answer questions about the quantity of things",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"ask and answer questions about the quantity of things"
],
"grammar": "How many clocks are there? There is one clock.\nHow many pictures are there? There are three pictures."
}
},
{
"chapter_id": "uuid_chapter_11_grade3",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Exam preparation for Unit 11",
"lesson_content_type": "review"
}
]

View File

@@ -0,0 +1,42 @@
[
{
"chapter_id": "uuid_chapter_12_grade3",
"lesson_number": 1,
"lesson_title": "Lesson 1: Action Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary about actions and understand a story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify the vocabulary about the actions",
"understand a story"
],
"vocabulary": ["learn", "read", "write", "spell", "draw"]
}
},
{
"chapter_id": "uuid_chapter_12_grade3",
"lesson_number": 2,
"lesson_title": "Lesson 2: Questions About Abilities",
"lesson_type": "json_content",
"lesson_description": "Ask and answer questions about someone's abilities",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"ask and answer questions about someone's abilities",
"match the words with the correct pictures"
],
"grammar": "Can you spell your name? Yes, I can.\nCan you draw a picture? No, I can't."
}
},
{
"chapter_id": "uuid_chapter_12_grade3",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Exam preparation for Unit 12",
"lesson_content_type": "review"
}
]

42
data/moveup/g3/unit7.json Normal file
View File

@@ -0,0 +1,42 @@
[
{
"chapter_id": "uuid_chapter_7_grade3",
"lesson_number": 1,
"lesson_title": "Lesson 1: Clothing Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify different types of clothing and understand a story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify different types of clothing",
"understand a story"
],
"vocabulary": ["baseball caps", "jackets", "trousers", "glasses"]
}
},
{
"chapter_id": "uuid_chapter_7_grade3",
"lesson_number": 2,
"lesson_title": "Lesson 2: Talking About Clothing",
"lesson_type": "json_content",
"lesson_description": "Talk about what people are wearing and match sentences with pictures",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"talk about what people are wearing",
"read and match the sentences with the correct pictures"
],
"grammar": "What are they wearing? They're wearing baseball caps."
}
},
{
"chapter_id": "uuid_chapter_7_grade3",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Exam preparation for Unit 7",
"lesson_content_type": "review"
}
]

42
data/moveup/g3/unit8.json Normal file
View File

@@ -0,0 +1,42 @@
[
{
"chapter_id": "uuid_chapter_8_grade3",
"lesson_number": 1,
"lesson_title": "Lesson 1: Activities Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify different activities people are doing and understand a story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify different activities people are doing",
"understand a story"
],
"vocabulary": ["drink milk", "make a cake", "listen to music", "play drums"]
}
},
{
"chapter_id": "uuid_chapter_8_grade3",
"lesson_number": 2,
"lesson_title": "Lesson 2: Questions About Activities",
"lesson_type": "json_content",
"lesson_description": "Ask and answer questions about what people are doing",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"ask and answer questions about what people are doing",
"read and number the sentences with suitable pictures"
],
"grammar": "What are they doing? They are making a cake."
}
},
{
"chapter_id": "uuid_chapter_8_grade3",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Exam preparation for Unit 8",
"lesson_content_type": "review"
}
]

42
data/moveup/g3/unit9.json Normal file
View File

@@ -0,0 +1,42 @@
[
{
"chapter_id": "uuid_chapter_9_grade3",
"lesson_number": 1,
"lesson_title": "Lesson 1: Abilities Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary about animal's ability or someone's ability",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify the vocabulary about animal's ability or someone's ability",
"understand a story"
],
"vocabulary": ["swim", "climb a tree", "draw a picture", "bounce a ball"]
}
},
{
"chapter_id": "uuid_chapter_9_grade3",
"lesson_number": 2,
"lesson_title": "Lesson 2: Questions About Abilities",
"lesson_type": "json_content",
"lesson_description": "Ask and answer questions about animal's ability or someone's ability",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"ask and answer questions about an animal's ability or someone's ability",
"read and tick the correct answers about animal's ability or someone's ability"
],
"grammar": "Can the duck swim? Yes, it can.\nCan it climb the tree? No, it can't."
}
},
{
"chapter_id": "uuid_chapter_9_grade3",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Exam preparation for Unit 9",
"lesson_content_type": "review"
}
]

View File

@@ -0,0 +1,50 @@
[
{
"chapter_id": "uuid_chapter_10_grade4",
"lesson_number": 1,
"lesson_title": "Lesson 1: Frog Life Cycle",
"lesson_type": "json_content",
"lesson_description": "Identify frog life cycle and understand a short story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify frog life cycle",
"understand a short story"
],
"vocabulary": ["egg", "tadpole", "tadpole with legs", "froglet", "mature frog"]
}
},
{
"chapter_id": "uuid_chapter_10_grade4",
"lesson_number": 2,
"lesson_title": "Lesson 2: Using Is/Was",
"lesson_type": "json_content",
"lesson_description": "Use is/was in sentences about life cycles",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify frog life cycle",
"review adjectives",
"use is/was in sentences"
],
"grammar": "A frog was a tadpole before."
}
},
{
"chapter_id": "uuid_chapter_10_grade4",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Review previous lessons",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review previous lessons",
"listen for specific information"
]
}
}
]

View File

@@ -0,0 +1,49 @@
[
{
"chapter_id": "uuid_chapter_11_grade4",
"lesson_number": 1,
"lesson_title": "Lesson 1: Past Tense Verbs",
"lesson_type": "json_content",
"lesson_description": "Identify action verbs and understand a short story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify action verbs",
"understand a short story"
],
"vocabulary": ["cast/ cast", "lose/ lost", "tell/ told", "think/ thought", "break/ broke"]
}
},
{
"chapter_id": "uuid_chapter_11_grade4",
"lesson_number": 2,
"lesson_title": "Lesson 2: Simple Past Tense",
"lesson_type": "json_content",
"lesson_description": "Use verbs in simple past tense",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"review action verbs",
"use verbs in simple past tense"
],
"grammar": "What did she do yesterday? She told a story to her daughter"
}
},
{
"chapter_id": "uuid_chapter_11_grade4",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Review previous lessons",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review previous lessons",
"listen for specific information"
]
}
}
]

View File

@@ -0,0 +1,49 @@
[
{
"chapter_id": "uuid_chapter_12_grade4",
"lesson_number": 1,
"lesson_title": "Lesson 1: Descriptive Adjectives",
"lesson_type": "json_content",
"lesson_description": "Identify adjectives to describe people and understand a short story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify different adjectives to describe people",
"understand a short story"
],
"vocabulary": ["fast", "slow", "big", "small", "cute"]
}
},
{
"chapter_id": "uuid_chapter_12_grade4",
"lesson_number": 2,
"lesson_title": "Lesson 2: Comparative Adjectives",
"lesson_type": "json_content",
"lesson_description": "Use comparative adjectives in sentences",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"review adjectives",
"use comparative adjectives in sentences"
],
"grammar": "I was faster than my friends."
}
},
{
"chapter_id": "uuid_chapter_12_grade4",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Review previous lessons",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review previous lessons",
"listen for specific information"
]
}
}
]

49
data/moveup/g4/unit7.json Normal file
View File

@@ -0,0 +1,49 @@
[
{
"chapter_id": "uuid_chapter_7_grade4",
"lesson_number": 1,
"lesson_title": "Lesson 1: Activities Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify different activities and understand a short story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify different activities",
"understand a short story"
],
"vocabulary": ["singing karaoke", "taking photos", "inviting friends", "having some fun"]
}
},
{
"chapter_id": "uuid_chapter_7_grade4",
"lesson_number": 2,
"lesson_title": "Lesson 2: Present Progressive Tense",
"lesson_type": "json_content",
"lesson_description": "Use present progressive tense in sentences",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify different activities",
"use present progressive tense in sentences"
],
"grammar": "I'm singing karaoke next week."
}
},
{
"chapter_id": "uuid_chapter_7_grade4",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Review different activities and things about birthdays",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review different activities and things about birthdays",
"find the correct answers"
]
}
}
]

50
data/moveup/g4/unit8.json Normal file
View File

@@ -0,0 +1,50 @@
[
{
"chapter_id": "uuid_chapter_8_grade4",
"lesson_number": 1,
"lesson_title": "Lesson 1: Cultural Activities",
"lesson_type": "json_content",
"lesson_description": "Identify different activities and understand a short story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify different activities",
"understand a short story"
],
"vocabulary": ["dance in traditional dresses", "pour water on the Buddha statue", "clean Buddha statue", "water fight"]
}
},
{
"chapter_id": "uuid_chapter_8_grade4",
"lesson_number": 2,
"lesson_title": "Lesson 2: Talking About Likes",
"lesson_type": "json_content",
"lesson_description": "Talk about likes",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify different activities",
"talk about likes"
],
"grammar": "He likes water fights."
}
},
{
"chapter_id": "uuid_chapter_8_grade4",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Review different activities and understand the invitation",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review different activities",
"answer questions",
"understand the invitation"
]
}
}
]

49
data/moveup/g4/unit9.json Normal file
View File

@@ -0,0 +1,49 @@
[
{
"chapter_id": "uuid_chapter_9_grade4",
"lesson_number": 1,
"lesson_title": "Lesson 1: Daily Routines and Time",
"lesson_type": "json_content",
"lesson_description": "Identify everyday actions and understand a short story",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify everyday actions",
"understand a short story"
],
"vocabulary": ["quarter past", "half past", "quarter to", "have a shower", "set the table", "wash the dishes", "take out the trash"]
}
},
{
"chapter_id": "uuid_chapter_9_grade4",
"lesson_number": 2,
"lesson_title": "Lesson 2: Daily Routine Questions",
"lesson_type": "json_content",
"lesson_description": "Ask and answer about daily routines",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify everyday actions",
"ask and answer what they do at different times of the day"
],
"grammar": "Do you have a shower at half past seven every day? Yes, I do. / No, I don't. I wash the dishes at half past seven."
}
},
{
"chapter_id": "uuid_chapter_9_grade4",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation",
"lesson_type": "json_content",
"lesson_description": "Review everyday actions",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review everyday actions",
"listen and tick the correct answers"
]
}
}
]

View File

@@ -0,0 +1,64 @@
[
{
"chapter_id": "uuid_chapter_10_grade5",
"lesson_number": 1,
"lesson_title": "Lesson 1: Illness Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary related to illness",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify vocabulary related to illness",
"understand a story"
],
"vocabulary": ["see a doctor", "go to the dentist's", "have a runny nose", "have a flu", "take medicine", "take a rest"]
}
},
{
"chapter_id": "uuid_chapter_10_grade5",
"lesson_number": 2,
"lesson_title": "Lesson 2: Asking for and Giving Advice",
"lesson_type": "json_content",
"lesson_description": "Ask for and give advice using should",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify how to ask for and give advice",
"use the patterns in actual communication"
],
"grammar": "You should … / What should I do?"
}
},
{
"chapter_id": "uuid_chapter_10_grade5",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation - Illness and Advice",
"lesson_type": "json_content",
"lesson_description": "Review language items and improve skills",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review the language items learnt",
"improve listening and speaking skills"
]
}
},
{
"chapter_id": "uuid_chapter_10_grade5",
"lesson_number": 4,
"lesson_title": "Lesson 4: Review and Practice",
"lesson_type": "json_content",
"lesson_description": "Review language items and improve skills",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review the language items learnt",
"improve reading and writing skills"
]
}
}
]

View File

@@ -0,0 +1,64 @@
[
{
"chapter_id": "uuid_chapter_11_grade5",
"lesson_number": 1,
"lesson_title": "Lesson 1: Housework Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary related to daily routines and housework",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify vocabulary related to daily routines and housework",
"understand a story"
],
"vocabulary": ["have a meeting", "arrive home", "clean the house", "dust the furniture", "do the laundry", "iron the clothes"]
}
},
{
"chapter_id": "uuid_chapter_11_grade5",
"lesson_number": 2,
"lesson_title": "Lesson 2: Simple Future Tense",
"lesson_type": "json_content",
"lesson_description": "Use simple future tense with will",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify simple future of regular verbs",
"use the patterns in actual communication"
],
"grammar": "What will he do? He'll … / Will you …? Yes/No, …"
}
},
{
"chapter_id": "uuid_chapter_11_grade5",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation - Future Tense",
"lesson_type": "json_content",
"lesson_description": "Review language items and improve skills",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review the language items learnt",
"improve listening, reading and speaking skills"
]
}
},
{
"chapter_id": "uuid_chapter_11_grade5",
"lesson_number": 4,
"lesson_title": "Lesson 4: Skills Practice",
"lesson_type": "json_content",
"lesson_description": "Review language items and improve skills",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review the language items learnt",
"improve reading and speaking skills"
]
}
}
]

View File

@@ -0,0 +1,64 @@
[
{
"chapter_id": "uuid_chapter_12_grade5",
"lesson_number": 1,
"lesson_title": "Lesson 1: Entertainment Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary related to entertainment and technology",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify vocabulary related to entertainment and technology",
"understand a story"
],
"vocabulary": ["game show", "documentary", "comedy", "concert", "art gallery", "circus"]
}
},
{
"chapter_id": "uuid_chapter_12_grade5",
"lesson_number": 2,
"lesson_title": "Lesson 2: Be Going To Future",
"lesson_type": "json_content",
"lesson_description": "Use be going to for future plans",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify simple future 'be going to'",
"use the patterns in actual communication"
],
"grammar": "What is he going to do next weekend? He's going to …."
}
},
{
"chapter_id": "uuid_chapter_12_grade5",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation - Future Plans",
"lesson_type": "json_content",
"lesson_description": "Review language items and improve skills",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review the language items learnt",
"improve listening, reading and speaking skills"
]
}
},
{
"chapter_id": "uuid_chapter_12_grade5",
"lesson_number": 4,
"lesson_title": "Lesson 4: Review and Writing Practice",
"lesson_type": "json_content",
"lesson_description": "Review language items and improve skills",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review the language items learnt",
"improve reading and writing skills"
]
}
}
]

66
data/moveup/g5/unit7.json Normal file
View File

@@ -0,0 +1,66 @@
[
{
"chapter_id": "uuid_chapter_7_grade5",
"lesson_number": 1,
"lesson_title": "Lesson 1: Travel and Vacation Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary related to travel and vacation",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify and pronounce new vocabulary related to travel and vacation",
"understand a short dialogue about vacation experiences"
],
"vocabulary": ["postcard", "gift shop", "suitcase", "forget", "bring", "send"]
}
},
{
"chapter_id": "uuid_chapter_7_grade5",
"lesson_number": 2,
"lesson_title": "Lesson 2: Simple Past Tense - Irregular Verbs",
"lesson_type": "json_content",
"lesson_description": "Use simple past tense with irregular verbs",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify the simple past tense of common irregular verbs",
"use irregular verbs correctly in simple sentences"
],
"grammar": "S + Irregular Verb (Past form) + O. Example: She sent some postcards to her friends when she was on vacation"
}
},
{
"chapter_id": "uuid_chapter_7_grade5",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation - Past Tense",
"lesson_type": "json_content",
"lesson_description": "Review simple past tense and listening skills",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review vocabulary and simple past grammar structures",
"improve listening skills",
"identify key information in dialogues"
]
}
},
{
"chapter_id": "uuid_chapter_7_grade5",
"lesson_number": 4,
"lesson_title": "Lesson 4: Travel Questions Practice",
"lesson_type": "json_content",
"lesson_description": "Review travel vocabulary and WH-questions in past tense",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review vocabulary and simple past grammar structures",
"improve reading skills",
"apply simple past tense in conversational context"
]
}
}
]

64
data/moveup/g5/unit8.json Normal file
View File

@@ -0,0 +1,64 @@
[
{
"chapter_id": "uuid_chapter_8_grade5",
"lesson_number": 1,
"lesson_title": "Lesson 1: Structures Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary related to natural and man-made structures",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify and pronounce new vocabulary related to structures",
"understand a short dialogue discussing superlatives"
],
"vocabulary": ["bridge", "castle", "cave", "desert", "pond"]
}
},
{
"chapter_id": "uuid_chapter_8_grade5",
"lesson_number": 2,
"lesson_title": "Lesson 2: Superlative Adjectives",
"lesson_type": "json_content",
"lesson_description": "Use superlative forms to ask and answer questions",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify and formulate the superlative form of adjectives",
"use superlative structures to ask and answer questions"
],
"grammar": "What's the + Superlative + Noun + in the world/family/country? It's + Proper Noun / It's + Adjective + Noun."
}
},
{
"chapter_id": "uuid_chapter_8_grade5",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation - Superlatives",
"lesson_type": "json_content",
"lesson_description": "Review superlative grammar structures",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review vocabulary and superlative grammar structures",
"improve listening and reading skills"
]
}
},
{
"chapter_id": "uuid_chapter_8_grade5",
"lesson_number": 4,
"lesson_title": "Lesson 4: Reading Comprehension with Superlatives",
"lesson_type": "json_content",
"lesson_description": "Apply superlative structures in extended reading contexts",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review superlative structures in reading contexts",
"improve reading comprehension skills"
]
}
}
]

64
data/moveup/g5/unit9.json Normal file
View File

@@ -0,0 +1,64 @@
[
{
"chapter_id": "uuid_chapter_9_grade5",
"lesson_number": 1,
"lesson_title": "Lesson 1: Public Place Rules Vocabulary",
"lesson_type": "json_content",
"lesson_description": "Identify vocabulary related to rules in public places",
"lesson_content_type": "vocabulary",
"content_json": {
"type": "vocabulary",
"learning_objectives": [
"identify and pronounce vocabulary phrases related to rules",
"understand a dialogue about movie theater rules"
],
"vocabulary": ["throw garbage", "have a seat", "bring food", "stand in line", "talk on the phone", "record a movie"]
}
},
{
"chapter_id": "uuid_chapter_9_grade5",
"lesson_number": 2,
"lesson_title": "Lesson 2: Adverbs of Manner",
"lesson_type": "json_content",
"lesson_description": "Use adverbs of manner to describe actions",
"lesson_content_type": "grammar",
"content_json": {
"type": "grammar",
"learning_objectives": [
"identify and formulate adverbs of manner",
"use adverbs of manner correctly to describe actions"
],
"grammar": "Adverbs of Manner: carefully, quietly, quickly, loudly, neatly, fast"
}
},
{
"chapter_id": "uuid_chapter_9_grade5",
"lesson_number": 3,
"lesson_title": "Lesson 3: Exam Preparation - Rules and Adverbs",
"lesson_type": "json_content",
"lesson_description": "Review vocabulary and grammar from Unit 9",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review vocabulary and grammar structures",
"improve listening skills"
]
}
},
{
"chapter_id": "uuid_chapter_9_grade5",
"lesson_number": 4,
"lesson_title": "Lesson 4: Vocabulary and Sentence Completion",
"lesson_type": "json_content",
"lesson_description": "Review vocabulary and phrases for sentence completion",
"lesson_content_type": "review",
"content_json": {
"type": "review",
"learning_objectives": [
"review vocabulary and phrases",
"improve reading and vocabulary skills"
]
}
}
]

84
models/Grammar.js Normal file
View File

@@ -0,0 +1,84 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Grammar = sequelize.define('Grammar', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
grammar_code: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
comment: 'Unique identifier for grammar rule (e.g., "gram-001-present-cont")'
},
title: {
type: DataTypes.STRING(200),
allowNull: false,
comment: 'Grammar rule name (e.g., "Present Continuous")'
},
translation: {
type: DataTypes.STRING(200),
allowNull: true,
comment: 'Vietnamese translation'
},
// GENERATIVE LOGIC
structure: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: {},
comment: 'Formula and pattern_logic for sentence generation. Structure: { formula: string, pattern_logic: array }'
},
// METADATA
instructions: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: {},
comment: 'Instructions and hints. Structure: { vi: string, hint: string }'
},
// ATTRIBUTES
difficulty_score: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 1,
validate: {
min: 1,
max: 10
},
comment: 'Difficulty level from 1 (easiest) to 10 (hardest)'
},
category: {
type: DataTypes.STRING(100),
allowNull: true,
comment: 'Grammar category (e.g., "Tenses", "Modal Verbs", "Questions")'
},
tags: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of tags for categorization'
},
// STATUS
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Soft delete flag'
}
}, {
tableName: 'grammars',
timestamps: true,
indexes: [
{ fields: ['grammar_code'], unique: true },
{ fields: ['category'] },
{ fields: ['difficulty_score'] },
{ fields: ['is_active'] }
]
});
module.exports = Grammar;

56
models/GrammarMapping.js Normal file
View File

@@ -0,0 +1,56 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const GrammarMapping = sequelize.define('GrammarMapping', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
grammar_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'grammars',
key: 'id'
},
onDelete: 'CASCADE',
comment: 'Foreign key to grammars table'
},
book_id: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Book identifier (e.g., "global-success-2")'
},
grade: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Grade level'
},
unit: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Unit number in the book'
},
lesson: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Lesson number within the unit'
},
context_note: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Additional context about where this grammar appears'
}
}, {
tableName: 'grammar_mappings',
timestamps: true,
indexes: [
{ fields: ['grammar_id'] },
{ fields: ['book_id'] },
{ fields: ['grade'] },
{ fields: ['book_id', 'grade', 'unit', 'lesson'] }
]
});
module.exports = GrammarMapping;

View File

@@ -0,0 +1,71 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const GrammarMediaStory = sequelize.define('GrammarMediaStory', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
grammar_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'grammars',
key: 'id'
},
onDelete: 'CASCADE',
comment: 'Foreign key to grammars table'
},
story_id: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Unique story identifier (e.g., "st-01")'
},
title: {
type: DataTypes.STRING(200),
allowNull: false,
comment: 'Story title'
},
type: {
type: DataTypes.ENUM('story', 'video', 'animation', 'audio'),
allowNull: false,
defaultValue: 'story',
comment: 'Media type'
},
url: {
type: DataTypes.STRING(500),
allowNull: false,
comment: 'Single URL to the complete media file'
},
thumbnail: {
type: DataTypes.STRING(500),
allowNull: true,
comment: 'Thumbnail image URL'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Story description'
},
duration_seconds: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Media duration in seconds'
},
min_grade: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Minimum grade level for this story'
}
}, {
tableName: 'grammar_media_stories',
timestamps: true,
indexes: [
{ fields: ['grammar_id'] },
{ fields: ['story_id'] },
{ fields: ['type'] }
]
});
module.exports = GrammarMediaStory;

View File

@@ -37,9 +37,21 @@ const Lesson = sequelize.define('lessons', {
},
// Dạng 1: JSON Content - Nội dung học tập dạng JSON
// Cấu trúc content_json phụ thuộc vào lesson_content_type:
// - vocabulary: { type: "vocabulary", vocabulary_ids: [uuid1, uuid2, ...], exercises: [...] }
// - grammar: { type: "grammar", grammar_ids: [uuid1, uuid2, ...], examples: [...], exercises: [...] }
// - phonics: { type: "phonics", phonics_rules: [{ipa: "/æ/", words: [...]}], exercises: [...] }
// - review: { type: "review", sections: [{type: "vocabulary", ...}, {type: "grammar", ...}, {type: "phonics", ...}] }
content_json: {
type: DataTypes.JSON,
comment: 'Nội dung học tập dạng JSON: text, quiz, interactive, assignment, etc.'
comment: 'Nội dung học tập dạng JSON: vocabulary, grammar, phonics, review'
},
// Loại nội dung của bài học (để query dễ dàng)
lesson_content_type: {
type: DataTypes.ENUM('vocabulary', 'grammar', 'phonics', 'review', 'mixed'),
allowNull: true,
comment: 'Loại nội dung: vocabulary, grammar, phonics, review, mixed'
},
// Dạng 2: URL Content - Chứa link external

81
models/Story.js Normal file
View File

@@ -0,0 +1,81 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
/**
* Story Model
* Table: stories
* Stores story data with metadata, vocabulary, context, grades, and tags
*/
const Story = sequelize.define('stories', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false,
comment: 'Unique identifier for the story (UUID)'
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
comment: 'Story title/name'
},
logo: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'URL to story logo/thumbnail image'
},
vocabulary: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of vocabulary words used in the story'
},
context: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of story context objects with images, text, audio data'
},
grade: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of grade levels (e.g., ["Grade 1", "Grade 2"])'
},
tag: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'Array of tags for categorization (e.g., ["adventure", "friendship"])'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: 'Record creation timestamp'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: 'Record last update timestamp'
}
}, {
tableName: 'stories',
timestamps: true,
underscored: true,
indexes: [
{
name: 'idx_name',
fields: ['name']
},
{
name: 'idx_created_at',
fields: ['created_at']
}
],
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci'
});
module.exports = Story;

90
models/Vocab.js Normal file
View File

@@ -0,0 +1,90 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Vocab = sequelize.define('Vocab', {
vocab_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for vocabulary entry'
},
vocab_code: {
type: DataTypes.STRING(50),
unique: true,
allowNull: false,
comment: 'Unique code for vocabulary (e.g., vocab-001-eat)'
},
base_word: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Base form of the word'
},
translation: {
type: DataTypes.STRING(200),
allowNull: false,
comment: 'Vietnamese translation'
},
difficulty_score: {
type: DataTypes.INTEGER,
defaultValue: 1,
comment: 'Difficulty level (1-10)'
},
category: {
type: DataTypes.STRING(100),
comment: 'Category of the word (e.g., Action Verbs, Nouns)'
},
images: {
type: DataTypes.JSON,
comment: 'Array of image URLs'
},
tags: {
type: DataTypes.JSON,
comment: 'Array of tags for categorization'
},
syntax: {
type: DataTypes.JSON,
comment: 'Syntax roles for Grammar Engine (is_subject, is_verb, is_object, is_be, is_adj, is_adv, is_article, verb_type, article_type, adv_type, position, priority, etc.)'
},
semantics: {
type: DataTypes.JSON,
comment: 'Semantic constraints (can_be_subject_type, can_take_object_type, can_modify, cannot_modify, word_type, is_countable, person_type, etc.)'
},
constraints: {
type: DataTypes.JSON,
comment: 'Grammar constraints (followed_by, match_subject, match_with, phonetic_rules, etc.)'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether this vocab entry is active'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
name: 'idx_vocab_code',
fields: ['vocab_code']
},
{
name: 'idx_base_word',
fields: ['base_word']
},
{
name: 'idx_category',
fields: ['category']
}
]
});
module.exports = Vocab;

78
models/VocabForm.js Normal file
View File

@@ -0,0 +1,78 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const VocabForm = sequelize.define('VocabForm', {
form_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for word form'
},
vocab_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'CASCADE',
comment: 'Reference to vocabulary entry'
},
form_key: {
type: DataTypes.STRING(50),
allowNull: false,
comment: 'Form identifier (v1, v2, v3, v_s_es, v_ing, etc.)'
},
text: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'The actual word form (e.g., eat, eats, eating, ate)'
},
phonetic: {
type: DataTypes.STRING(100),
comment: 'IPA phonetic transcription (e.g., /iːt/)'
},
audio_url: {
type: DataTypes.STRING(500),
comment: 'URL to audio pronunciation file'
},
min_grade: {
type: DataTypes.INTEGER,
defaultValue: 1,
comment: 'Minimum grade level to unlock this form'
},
description: {
type: DataTypes.TEXT,
comment: 'Description or usage note for this form'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab_form',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
name: 'idx_vocab_form_vocab',
fields: ['vocab_id']
},
{
name: 'idx_vocab_form_key',
fields: ['form_key']
},
{
name: 'idx_vocab_form_unique',
unique: true,
fields: ['vocab_id', 'form_key']
}
]
});
module.exports = VocabForm;

72
models/VocabMapping.js Normal file
View File

@@ -0,0 +1,72 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const VocabMapping = sequelize.define('VocabMapping', {
mapping_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for curriculum mapping'
},
vocab_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'CASCADE',
comment: 'Reference to vocabulary entry'
},
book_id: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Book/curriculum identifier (e.g., global-success-1)'
},
grade: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Grade level'
},
unit: {
type: DataTypes.INTEGER,
comment: 'Unit number in the book'
},
lesson: {
type: DataTypes.INTEGER,
comment: 'Lesson number in the unit'
},
form_key: {
type: DataTypes.STRING(50),
comment: 'Which form to use (v1, v_s_es, v_ing, v2, v3, etc.)'
},
context_note: {
type: DataTypes.TEXT,
comment: 'Additional context for this mapping'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab_mapping',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
name: 'idx_vocab_mapping_vocab',
fields: ['vocab_id']
},
{
name: 'idx_vocab_mapping_book_grade',
fields: ['book_id', 'grade']
},
{
name: 'idx_vocab_mapping_unit_lesson',
fields: ['unit', 'lesson']
}
]
});
module.exports = VocabMapping;

65
models/VocabRelation.js Normal file
View File

@@ -0,0 +1,65 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const VocabRelation = sequelize.define('VocabRelation', {
relation_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
comment: 'Unique identifier for vocabulary relation'
},
vocab_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'CASCADE',
comment: 'Reference to vocabulary entry'
},
relation_type: {
type: DataTypes.ENUM('synonym', 'antonym', 'related'),
allowNull: false,
comment: 'Type of relation (synonym, antonym, related)'
},
related_word: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'The related word (e.g., consume, dine, fast)'
},
related_vocab_id: {
type: DataTypes.UUID,
references: {
model: 'vocab',
key: 'vocab_id'
},
onDelete: 'SET NULL',
comment: 'Reference to related vocab entry if it exists in system'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'vocab_relation',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
name: 'idx_vocab_relation_vocab',
fields: ['vocab_id']
},
{
name: 'idx_vocab_relation_type',
fields: ['relation_type']
},
{
name: 'idx_vocab_relation_related',
fields: ['related_vocab_id']
}
]
});
module.exports = VocabRelation;

View File

@@ -32,6 +32,20 @@ const Game = require('./Game');
const LessonComponentProgress = require('./LessonComponentProgress');
const LessonLeaderboard = require('./LessonLeaderboard');
// Group 3.2: Vocabulary System (NEW)
const Vocab = require('./Vocab');
const VocabMapping = require('./VocabMapping');
const VocabForm = require('./VocabForm');
const VocabRelation = require('./VocabRelation');
// Group 3.3: Grammar System (NEW)
const Grammar = require('./Grammar');
const GrammarMapping = require('./GrammarMapping');
const GrammarMediaStory = require('./GrammarMediaStory');
// Group 3.4: Story System (NEW)
const Story = require('./Story');
// Group 4: Attendance
const AttendanceLog = require('./AttendanceLog');
const AttendanceDaily = require('./AttendanceDaily');
@@ -148,6 +162,29 @@ const setupRelationships = () => {
LessonLeaderboard.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
Lesson.hasMany(LessonLeaderboard, { foreignKey: 'lesson_id', as: 'leaderboard' });
// Vocabulary relationships (NEW)
// Vocab -> VocabMapping (1:N)
Vocab.hasMany(VocabMapping, { foreignKey: 'vocab_id', as: 'mappings' });
VocabMapping.belongsTo(Vocab, { foreignKey: 'vocab_id', as: 'vocab' });
// Vocab -> VocabForm (1:N)
Vocab.hasMany(VocabForm, { foreignKey: 'vocab_id', as: 'forms' });
VocabForm.belongsTo(Vocab, { foreignKey: 'vocab_id', as: 'vocab' });
// Vocab -> VocabRelation (1:N)
Vocab.hasMany(VocabRelation, { foreignKey: 'vocab_id', as: 'relations' });
VocabRelation.belongsTo(Vocab, { foreignKey: 'vocab_id', as: 'vocab' });
VocabRelation.belongsTo(Vocab, { foreignKey: 'related_vocab_id', as: 'relatedVocab' });
// Grammar relationships (NEW)
// Grammar -> GrammarMapping (1:N)
Grammar.hasMany(GrammarMapping, { foreignKey: 'grammar_id', as: 'mappings' });
GrammarMapping.belongsTo(Grammar, { foreignKey: 'grammar_id', as: 'grammar' });
// Grammar -> GrammarMediaStory (1:N)
Grammar.hasMany(GrammarMediaStory, { foreignKey: 'grammar_id', as: 'mediaStories' });
GrammarMediaStory.belongsTo(Grammar, { foreignKey: 'grammar_id', as: 'grammar' });
// Attendance relationships
AttendanceLog.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
AttendanceLog.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
@@ -258,6 +295,20 @@ module.exports = {
LessonComponentProgress,
LessonLeaderboard,
// Group 3.2: Vocabulary System (NEW)
Vocab,
VocabMapping,
VocabForm,
VocabRelation,
// Group 3.3: Grammar System (NEW)
Grammar,
GrammarMapping,
GrammarMediaStory,
// Group 3.4: Story System (NEW)
Story,
// Group 4: Attendance
AttendanceLog,
AttendanceDaily,

433
routes/grammarRoutes.js Normal file
View File

@@ -0,0 +1,433 @@
const express = require('express');
const router = express.Router();
const grammarController = require('../controllers/grammarController');
const { authenticateToken } = require('../middleware/auth');
/**
* @swagger
* tags:
* name: Grammar
* description: Grammar rule management system for sentence generation
*/
/**
* @swagger
* /api/grammar:
* post:
* summary: Create a new grammar rule
* tags: [Grammar]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - grammar_code
* - title
* - structure
* properties:
* grammar_code:
* type: string
* example: "gram-001-present-cont"
* title:
* type: string
* example: "Present Continuous"
* translation:
* type: string
* example: "Thì hiện tại tiếp diễn"
* structure:
* type: object
* required:
* - formula
* properties:
* formula:
* type: string
* example: "S + am/is/are + V-ing + (a/an) + O + Adv"
* pattern_logic:
* type: array
* items:
* type: object
* properties:
* slot_id:
* type: string
* role:
* type: string
* semantic_filter:
* type: array
* items:
* type: string
* use_form:
* type: string
* dependency:
* type: string
* is_optional:
* type: boolean
* position:
* type: string
* instructions:
* type: object
* properties:
* vi:
* type: string
* hint:
* type: string
* difficulty_score:
* type: integer
* minimum: 1
* maximum: 10
* category:
* type: string
* tags:
* type: array
* items:
* type: string
* mappings:
* type: array
* items:
* type: object
* properties:
* book_id:
* type: string
* grade:
* type: integer
* unit:
* type: integer
* lesson:
* type: integer
* context_note:
* type: string
* media_stories:
* type: array
* items:
* type: object
* properties:
* story_id:
* type: string
* title:
* type: string
* type:
* type: string
* enum: [story, video, animation, audio]
* url:
* type: string
* thumbnail:
* type: string
* description:
* type: string
* duration_seconds:
* type: integer
* min_grade:
* type: integer
* example:
* grammar_code: "gram-001-present-cont"
* title: "Present Continuous"
* translation: "Thì hiện tại tiếp diễn"
* structure:
* formula: "S + am/is/are + V-ing + (a/an) + O + Adv"
* pattern_logic:
* - slot_id: "S_01"
* role: "is_subject"
* semantic_filter: ["human", "animal"]
* - slot_id: "BE_01"
* role: "is_be"
* dependency: "S_01"
* - slot_id: "V_01"
* role: "is_verb"
* use_form: "v_ing"
* semantic_filter: ["action"]
* - slot_id: "ART_01"
* role: "is_article"
* dependency: "O_01"
* is_optional: true
* - slot_id: "O_01"
* role: "is_object"
* semantic_match: "V_01"
* - slot_id: "ADV_01"
* role: "is_adv"
* is_optional: true
* position: "end"
* instructions:
* vi: "Dùng để nói về hành động đang diễn ra."
* hint: "Cấu trúc: Be + V-ing"
* difficulty_score: 2
* category: "Tenses"
* tags: ["present", "continuous", "action"]
* mappings:
* - book_id: "global-success-2"
* grade: 2
* unit: 5
* lesson: 1
* media_stories:
* - story_id: "st-01"
* title: "The Greedy Cat"
* type: "story"
* url: "https://cdn.sena.tech/stories/the-greedy-cat-full.mp4"
* thumbnail: "https://cdn.sena.tech/thumbs/greedy-cat.jpg"
* description: "A story about a cat who loves eating everything."
* duration_seconds: 180
* min_grade: 2
* responses:
* 201:
* description: Grammar rule created successfully
* 400:
* description: Invalid input
* 500:
* description: Server error
*/
router.post('/', authenticateToken, grammarController.createGrammar);
/**
* @swagger
* /api/grammar:
* get:
* summary: Get all grammar rules with pagination and filters
* tags: [Grammar]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Page number
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* description: Items per page
* - in: query
* name: category
* schema:
* type: string
* description: Filter by category (e.g., "Tenses", "Modal Verbs")
* - in: query
* name: grade
* schema:
* type: integer
* description: Filter by grade level
* - in: query
* name: book_id
* schema:
* type: string
* description: Filter by book ID (e.g., "global-success-1")
* - in: query
* name: difficulty_min
* schema:
* type: integer
* description: Minimum difficulty score
* - in: query
* name: difficulty_max
* schema:
* type: integer
* description: Maximum difficulty score
* - in: query
* name: search
* schema:
* type: string
* description: Search in title, translation, or grammar_code
* - in: query
* name: include_media
* schema:
* type: string
* enum: ['true', 'false']
* default: 'false'
* description: Include media stories in response
* responses:
* 200:
* description: List of grammar rules
* 500:
* description: Server error
*/
router.get('/', authenticateToken, grammarController.getAllGrammars);
/**
* @swagger
* /api/grammar/curriculum:
* get:
* summary: Get grammar rules by curriculum mapping
* tags: [Grammar]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: book_id
* required: false
* schema:
* type: string
* description: Book ID (e.g., "global-success-1")
* - in: query
* name: grade
* required: false
* schema:
* type: integer
* description: Grade level
* - in: query
* name: unit
* schema:
* type: integer
* description: Unit number
* - in: query
* name: lesson
* schema:
* type: integer
* description: Lesson number
* responses:
* 200:
* description: List of grammar rules for the specified curriculum
* 400:
* description: Invalid parameters
* 500:
* description: Server error
*/
router.get('/curriculum', authenticateToken, grammarController.getGrammarsByCurriculum);
/**
* @swagger
* /api/grammar/guide:
* get:
* summary: Get comprehensive guide for AI to create grammar rules
* tags: [Grammar]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Complete guide with rules, examples, and data structures
* content:
* application/json:
* schema:
* type: object
* properties:
* guide_version:
* type: string
* last_updated:
* type: string
* data_structure:
* type: object
* pattern_logic_roles:
* type: object
* semantic_filters:
* type: object
* form_keys_reference:
* type: object
* rules:
* type: object
* examples:
* type: object
* validation_checklist:
* type: array
* common_mistakes:
* type: array
* ai_tips:
* type: object
* 500:
* description: Server error
*/
router.get('/guide', authenticateToken, grammarController.getGrammarGuide);
/**
* @swagger
* /api/grammar/stats:
* get:
* summary: Get grammar statistics
* tags: [Grammar]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Grammar statistics
* 500:
* description: Server error
*/
router.get('/stats', authenticateToken, grammarController.getGrammarStats);
/**
* @swagger
* /api/grammar/{id}:
* get:
* summary: Get grammar rule by ID or code
* tags: [Grammar]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Grammar ID (numeric) or grammar_code (string)
* responses:
* 200:
* description: Grammar rule details
* 404:
* description: Grammar rule not found
* 500:
* description: Server error
*/
router.get('/:id', authenticateToken, grammarController.getGrammarById);
/**
* @swagger
* /api/grammar/{id}:
* put:
* summary: Update grammar rule
* tags: [Grammar]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Grammar ID (numeric) or grammar_code (string)
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* example:
* translation: "Thì hiện tại tiếp diễn (cập nhật)"
* difficulty_score: 3
* tags: ["present", "continuous", "action", "updated"]
* responses:
* 200:
* description: Grammar rule updated successfully
* 404:
* description: Grammar rule not found
* 500:
* description: Server error
*/
router.put('/:id', authenticateToken, grammarController.updateGrammar);
/**
* @swagger
* /api/grammar/{id}:
* delete:
* summary: Delete grammar rule (soft delete)
* tags: [Grammar]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Grammar ID (numeric) or grammar_code (string)
* responses:
* 200:
* description: Grammar rule deleted successfully
* 404:
* description: Grammar rule not found
* 500:
* description: Server error
*/
router.delete('/:id', authenticateToken, grammarController.deleteGrammar);
module.exports = router;

View File

@@ -0,0 +1,273 @@
const express = require('express');
const router = express.Router();
const learningContentController = require('../controllers/learningContentController');
const { authenticateToken } = require('../middleware/auth');
/**
* @swagger
* tags:
* name: Learning Content
* description: Learning content management (Subject → Chapter → Lesson)
*/
/**
* @swagger
* /api/learning-content/guide:
* get:
* summary: Get comprehensive learning content guide for AI
* tags: [Learning Content]
* description: Returns complete guide for Subject → Chapter → Lesson hierarchy and content structure
* responses:
* 200:
* description: Learning content guide
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* guide_version:
* type: string
* hierarchy:
* type: object
* subject_structure:
* type: object
* chapter_structure:
* type: object
* lesson_structure:
* type: object
* lesson_content_types:
* type: object
*/
router.get('/guide', learningContentController.getLearningContentGuide);
/**
* @swagger
* /api/learning-content/lessons:
* post:
* summary: Create new lesson
* tags: [Learning Content]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - chapter_id
* - lesson_number
* - lesson_title
* properties:
* chapter_id:
* type: string
* format: uuid
* description: Parent chapter UUID
* lesson_number:
* type: integer
* description: Sequential lesson number
* lesson_title:
* type: string
* maxLength: 200
* lesson_type:
* type: string
* enum: [json_content, url_content]
* default: json_content
* lesson_description:
* type: string
* lesson_content_type:
* type: string
* enum: [vocabulary, grammar, phonics, review, mixed]
* content_json:
* type: object
* description: JSON content structure (see guide for details)
* content_url:
* type: string
* content_type:
* type: string
* enum: [video, audio, pdf, image, interactive]
* duration_minutes:
* type: integer
* is_published:
* type: boolean
* default: false
* is_free:
* type: boolean
* default: false
* display_order:
* type: integer
* default: 0
* thumbnail_url:
* type: string
* responses:
* 201:
* description: Lesson created successfully
* 400:
* description: Validation error
* 404:
* description: Chapter not found
*/
router.post('/lessons', authenticateToken, learningContentController.createLesson);
/**
* @swagger
* /api/learning-content/lessons:
* get:
* summary: Get all lessons with pagination and filters
* tags: [Learning Content]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* - in: query
* name: chapter_id
* schema:
* type: string
* format: uuid
* description: Filter by chapter
* - in: query
* name: lesson_content_type
* schema:
* type: string
* enum: [vocabulary, grammar, phonics, review, mixed]
* description: Filter by content type
* - in: query
* name: lesson_type
* schema:
* type: string
* enum: [json_content, url_content]
* - in: query
* name: is_published
* schema:
* type: boolean
* - in: query
* name: is_free
* schema:
* type: boolean
* - in: query
* name: search
* schema:
* type: string
* description: Search in title and description
* responses:
* 200:
* description: List of lessons with pagination
*/
router.get('/lessons', learningContentController.getAllLessons);
/**
* @swagger
* /api/learning-content/lessons/{id}:
* get:
* summary: Get lesson by ID
* tags: [Learning Content]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Lesson details
* 404:
* description: Lesson not found
*/
router.get('/lessons/:id', learningContentController.getLessonById);
/**
* @swagger
* /api/learning-content/lessons/{id}:
* put:
* summary: Update lesson
* tags: [Learning Content]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* lesson_title:
* type: string
* lesson_description:
* type: string
* content_json:
* type: object
* is_published:
* type: boolean
* is_free:
* type: boolean
* responses:
* 200:
* description: Lesson updated successfully
* 404:
* description: Lesson not found
*/
router.put('/lessons/:id', authenticateToken, learningContentController.updateLesson);
/**
* @swagger
* /api/learning-content/lessons/{id}:
* delete:
* summary: Delete lesson
* tags: [Learning Content]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Lesson deleted successfully
* 404:
* description: Lesson not found
*/
router.delete('/lessons/:id', authenticateToken, learningContentController.deleteLesson);
/**
* @swagger
* /api/learning-content/lessons/chapter/{chapter_id}:
* get:
* summary: Get all lessons in a chapter
* tags: [Learning Content]
* parameters:
* - in: path
* name: chapter_id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: List of lessons in chapter
*/
router.get('/lessons/chapter/:chapter_id', learningContentController.getLessonsByChapter);
module.exports = router;

333
routes/storyRoutes.js Normal file
View File

@@ -0,0 +1,333 @@
const express = require('express');
const router = express.Router();
const storyController = require('../controllers/storyController');
const { authenticateToken } = require('../middleware/auth');
/**
* @swagger
* tags:
* name: Stories
* description: Story management system for interactive learning content
*/
/**
* @swagger
* /api/stories:
* post:
* summary: Create a new story
* tags: [Stories]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* properties:
* name:
* type: string
* example: "The Greedy Cat"
* logo:
* type: string
* example: "https://cdn.sena.tech/thumbs/greedy-cat.jpg"
* vocabulary:
* type: array
* items:
* type: string
* example: ["cat", "eat", "apple", "happy", "greedy"]
* context:
* type: array
* items:
* type: object
* properties:
* image:
* type: string
* text:
* type: string
* audio:
* type: string
* order:
* type: integer
* example:
* - image: "https://cdn.sena.tech/story/scene1.jpg"
* text: "Once upon a time, there was a greedy cat."
* audio: "https://cdn.sena.tech/audio/scene1.mp3"
* order: 1
* - image: "https://cdn.sena.tech/story/scene2.jpg"
* text: "The cat loved eating apples every day."
* audio: "https://cdn.sena.tech/audio/scene2.mp3"
* order: 2
* grade:
* type: array
* items:
* type: string
* example: ["Grade 1", "Grade 2"]
* tag:
* type: array
* items:
* type: string
* example: ["animals", "food", "lesson", "health", "fiction"]
* responses:
* 201:
* description: Story created successfully
* 400:
* description: Invalid input
* 500:
* description: Server error
*/
router.post('/', authenticateToken, storyController.createStory);
/**
* @swagger
* /api/stories:
* get:
* summary: Get all stories with pagination and filters
* tags: [Stories]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Page number
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* description: Items per page
* - in: query
* name: search
* schema:
* type: string
* description: Search in story name
* - in: query
* name: grade_filter
* schema:
* type: string
* description: Filter by grade (e.g., "Grade 1")
* - in: query
* name: tag_filter
* schema:
* type: string
* description: Filter by tag (e.g., "animals")
* - in: query
* name: sort_by
* schema:
* type: string
* default: created_at
* description: Sort by field
* - in: query
* name: sort_order
* schema:
* type: string
* enum: [ASC, DESC]
* default: DESC
* description: Sort order
* responses:
* 200:
* description: List of stories
* 500:
* description: Server error
*/
router.get('/', authenticateToken, storyController.getAllStories);
/**
* @swagger
* /api/stories/grade:
* get:
* summary: Get stories by grade level
* tags: [Stories]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: grade
* required: true
* schema:
* type: string
* description: Grade level (e.g., "Grade 1")
* responses:
* 200:
* description: List of stories for the specified grade
* 400:
* description: Missing grade parameter
* 500:
* description: Server error
*/
router.get('/grade', authenticateToken, storyController.getStoriesByGrade);
/**
* @swagger
* /api/stories/tag:
* get:
* summary: Get stories by tag
* tags: [Stories]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: tag
* required: true
* schema:
* type: string
* description: Tag name (e.g., "animals")
* responses:
* 200:
* description: List of stories with the specified tag
* 400:
* description: Missing tag parameter
* 500:
* description: Server error
*/
router.get('/tag', authenticateToken, storyController.getStoriesByTag);
/**
* @swagger
* /api/stories/guide:
* get:
* summary: Get comprehensive guide for AI to create stories
* tags: [Stories]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Complete guide with rules, examples, and data structures
* content:
* application/json:
* schema:
* type: object
* properties:
* guide_version:
* type: string
* last_updated:
* type: string
* data_structure:
* type: object
* context_structure:
* type: object
* vocabulary_guidelines:
* type: object
* grade_levels:
* type: object
* tag_categories:
* type: object
* examples:
* type: object
* validation_checklist:
* type: array
* common_mistakes:
* type: array
* ai_tips:
* type: object
* 500:
* description: Server error
*/
router.get('/guide', authenticateToken, storyController.getStoryGuide);
/**
* @swagger
* /api/stories/stats:
* get:
* summary: Get story statistics
* tags: [Stories]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Story statistics
* 500:
* description: Server error
*/
router.get('/stats', authenticateToken, storyController.getStoryStats);
/**
* @swagger
* /api/stories/{id}:
* get:
* summary: Get story by ID
* tags: [Stories]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Story ID (UUID)
* responses:
* 200:
* description: Story details
* 404:
* description: Story not found
* 500:
* description: Server error
*/
router.get('/:id', authenticateToken, storyController.getStoryById);
/**
* @swagger
* /api/stories/{id}:
* put:
* summary: Update story
* tags: [Stories]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Story ID (UUID)
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* example:
* name: "The Greedy Cat (Updated)"
* tag: ["animals", "food", "lesson", "health", "fiction", "updated"]
* responses:
* 200:
* description: Story updated successfully
* 404:
* description: Story not found
* 500:
* description: Server error
*/
router.put('/:id', authenticateToken, storyController.updateStory);
/**
* @swagger
* /api/stories/{id}:
* delete:
* summary: Delete story
* tags: [Stories]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Story ID (UUID)
* responses:
* 200:
* description: Story deleted successfully
* 404:
* description: Story not found
* 500:
* description: Server error
*/
router.delete('/:id', authenticateToken, storyController.deleteStory);
module.exports = router;

337
routes/vocabRoutes.js Normal file
View File

@@ -0,0 +1,337 @@
const express = require('express');
const router = express.Router();
const vocabController = require('../controllers/vocabController');
const { authenticateToken } = require('../middleware/auth');
/**
* @swagger
* tags:
* name: Vocabulary
* description: Vocabulary management system for curriculum-based language learning
*/
/**
* @swagger
* /api/vocab:
* post:
* summary: Create a new vocabulary entry
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/VocabComplete'
* example:
* vocab_code: "vocab-001-eat"
* base_word: "eat"
* translation: "ăn"
* attributes:
* difficulty_score: 1
* category: "Action Verbs"
* images:
* - "https://cdn.sena.tech/img/eat-main.png"
* - "https://cdn.sena.tech/img/eat-context.jpg"
* tags: ["daily-routine", "verb"]
* mappings:
* - book_id: "global-success-1"
* grade: 1
* unit: 2
* lesson: 3
* form_key: "v1"
* - book_id: "global-success-2"
* grade: 2
* unit: 5
* lesson: 1
* form_key: "v_ing"
* forms:
* v1:
* text: "eat"
* phonetic: "/iːt/"
* audio: "https://cdn.sena.tech/audio/eat_v1.mp3"
* min_grade: 1
* v_s_es:
* text: "eats"
* phonetic: "/iːts/"
* audio: "https://cdn.sena.tech/audio/eats_s.mp3"
* min_grade: 2
* v_ing:
* text: "eating"
* phonetic: "/ˈiː.tɪŋ/"
* audio: "https://cdn.sena.tech/audio/eating_ing.mp3"
* min_grade: 2
* v2:
* text: "ate"
* phonetic: "/et/"
* audio: "https://cdn.sena.tech/audio/ate_v2.mp3"
* min_grade: 3
* relations:
* synonyms: ["consume", "dine"]
* antonyms: ["fast", "starve"]
* syntax:
* is_subject: false
* is_verb: true
* is_object: false
* is_be: false
* is_adj: false
* verb_type: "transitive"
* semantics:
* can_be_subject_type: ["human", "animal"]
* can_take_object_type: ["food", "plant"]
* word_type: "action"
* constraints:
* requires_object: true
* semantic_object_types: ["food", "plant"]
* responses:
* 201:
* description: Vocabulary created successfully
* 400:
* description: Invalid input
* 500:
* description: Server error
*/
router.post('/', authenticateToken, vocabController.createVocab);
/**
* @swagger
* /api/vocab:
* get:
* summary: Get all vocabulary entries with pagination and filters
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Page number
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* description: Items per page
* - in: query
* name: category
* schema:
* type: string
* description: Filter by category (e.g., "Action Verbs")
* - in: query
* name: grade
* schema:
* type: integer
* description: Filter by grade level
* - in: query
* name: book_id
* schema:
* type: string
* description: Filter by book ID (e.g., "global-success-1")
* - in: query
* name: difficulty_min
* schema:
* type: integer
* description: Minimum difficulty score
* - in: query
* name: difficulty_max
* schema:
* type: integer
* description: Maximum difficulty score
* - in: query
* name: search
* schema:
* type: string
* description: Search in base_word, translation, or vocab_code
* - in: query
* name: include_relations
* schema:
* type: string
* enum: ['true', 'false']
* default: 'false'
* description: Include synonyms/antonyms in response
* responses:
* 200:
* description: List of vocabularies
* 500:
* description: Server error
*/
router.get('/', authenticateToken, vocabController.getAllVocabs);
/**
* @swagger
* /api/vocab/curriculum:
* get:
* summary: Get vocabularies by curriculum mapping
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: book_id
* required: false
* schema:
* type: string
* description: Book ID (e.g., "global-success-1")
* - in: query
* name: grade
* required: false
* schema:
* type: integer
* description: Grade level
* - in: query
* name: unit
* schema:
* type: integer
* description: Unit number
* - in: query
* name: lesson
* schema:
* type: integer
* description: Lesson number
* responses:
* 200:
* description: List of vocabularies for the specified curriculum
* 400:
* description: Invalid parameters
* 500:
* description: Server error
*/
router.get('/curriculum', authenticateToken, vocabController.getVocabsByCurriculum);
/**
* @swagger
* /api/vocab/guide:
* get:
* summary: Get comprehensive guide for AI to create vocabulary entries
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Complete guide with rules, examples, and data structures
* content:
* application/json:
* schema:
* type: object
* properties:
* guide_version:
* type: string
* last_updated:
* type: string
* data_structure:
* type: object
* rules:
* type: object
* examples:
* type: object
* 500:
* description: Server error
*/
router.get('/guide', authenticateToken, vocabController.getVocabGuide);
/**
* @swagger
* /api/vocab/stats:
* get:
* summary: Get vocabulary statistics
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Vocabulary statistics
* 500:
* description: Server error
*/
router.get('/stats', authenticateToken, vocabController.getVocabStats);
/**
* @swagger
* /api/vocab/{id}:
* get:
* summary: Get vocabulary by ID or code
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Vocabulary ID (numeric) or vocab_code (string)
* responses:
* 200:
* description: Vocabulary details
* 404:
* description: Vocabulary not found
* 500:
* description: Server error
*/
router.get('/:id', authenticateToken, vocabController.getVocabById);
/**
* @swagger
* /api/vocab/{id}:
* put:
* summary: Update vocabulary entry
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Vocabulary ID (numeric) or vocab_code (string)
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/VocabComplete'
* example:
* translation: "ăn uống"
* attributes:
* difficulty_score: 2
* tags: ["daily-routine", "verb", "food"]
* responses:
* 200:
* description: Vocabulary updated successfully
* 404:
* description: Vocabulary not found
* 500:
* description: Server error
*/
router.put('/:id', authenticateToken, vocabController.updateVocab);
/**
* @swagger
* /api/vocab/{id}:
* delete:
* summary: Delete vocabulary (soft delete)
* tags: [Vocabulary]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Vocabulary ID (numeric) or vocab_code (string)
* responses:
* 200:
* description: Vocabulary deleted successfully
* 404:
* description: Vocabulary not found
* 500:
* description: Server error
*/
router.delete('/:id', authenticateToken, vocabController.deleteVocab);
module.exports = router;

View File

@@ -0,0 +1,18 @@
const { sequelize } = require('../config/database');
async function addColumn() {
try {
await sequelize.query(`
ALTER TABLE lessons
ADD COLUMN lesson_content_type ENUM('vocabulary', 'grammar', 'phonics', 'review', 'mixed') NULL
COMMENT 'Loại nội dung: vocabulary, grammar, phonics, review, mixed'
`);
console.log('✅ Column lesson_content_type added successfully');
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
await sequelize.close();
}
}
addColumn();

View File

@@ -0,0 +1,237 @@
const fs = require('fs');
const path = require('path');
const { Subject, Chapter, Lesson, sequelize } = require('../models');
/**
* Import MoveUp curriculum data from data/moveup folder
* Structure: data/moveup/g1/unit4.json -> Subject: moveup_grade1 -> Chapter: Unit 4 -> Lessons
*/
async function importMoveUpData() {
const transaction = await sequelize.transaction();
try {
console.log('🚀 Starting MoveUp data import...\n');
const dataPath = path.join(__dirname, '../data/moveup');
const grades = ['g1', 'g2', 'g3', 'g4', 'g5'];
const stats = {
subjects: 0,
chapters: 0,
lessons: 0,
errors: []
};
for (const grade of grades) {
const gradePath = path.join(dataPath, grade);
// Check if grade folder exists
if (!fs.existsSync(gradePath)) {
console.log(`⏭️ Skipping ${grade} - folder not found`);
continue;
}
console.log(`📚 Processing ${grade.toUpperCase()}...`);
// Create Subject for this grade
const gradeNumber = grade.replace('g', '');
const subjectCode = `MOVEUP-G${gradeNumber}`;
const subjectName = `MoveUp Grade ${gradeNumber}`;
let subject = await Subject.findOne({
where: { subject_code: subjectCode },
transaction
});
if (!subject) {
subject = await Subject.create({
subject_code: subjectCode,
subject_name: subjectName,
subject_name_en: `MoveUp Grade ${gradeNumber}`,
description: `English curriculum for Grade ${gradeNumber} - MoveUp series`,
is_active: true,
is_public: true,
is_premium: false
}, { transaction });
stats.subjects++;
console.log(` ✅ Created Subject: ${subjectName} (${subject.id})`);
} else {
console.log(` ⏭️ Subject already exists: ${subjectName} (${subject.id})`);
}
// Read all JSON files in grade folder
const files = fs.readdirSync(gradePath).filter(f => f.endsWith('.json'));
for (const file of files) {
const filePath = path.join(gradePath, file);
const unitNumber = file.replace('unit', '').replace('.json', '');
console.log(` 📖 Processing ${file}...`);
// Create Chapter for this unit
const chapterTitle = `Unit ${unitNumber}`;
let chapter = await Chapter.findOne({
where: {
subject_id: subject.id,
chapter_number: parseInt(unitNumber)
},
transaction
});
if (!chapter) {
chapter = await Chapter.create({
subject_id: subject.id,
chapter_number: parseInt(unitNumber),
chapter_title: chapterTitle,
chapter_description: `Unit ${unitNumber} lessons`,
is_published: true,
display_order: parseInt(unitNumber)
}, { transaction });
stats.chapters++;
console.log(` ✅ Created Chapter: ${chapterTitle} (${chapter.id})`);
} else {
console.log(` ⏭️ Chapter already exists: ${chapterTitle} (${chapter.id})`);
}
// Read lessons from JSON file
let lessonsData;
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
lessonsData = JSON.parse(fileContent);
} catch (error) {
stats.errors.push(`Failed to read ${file}: ${error.message}`);
console.log(` ❌ Error reading ${file}: ${error.message}`);
continue;
}
// Create lessons
for (const lessonData of lessonsData) {
try {
// Check if lesson already exists
const existingLesson = await Lesson.findOne({
where: {
chapter_id: chapter.id,
lesson_number: lessonData.lesson_number
},
transaction
});
if (existingLesson) {
console.log(` ⏭️ Lesson ${lessonData.lesson_number} already exists: ${lessonData.lesson_title}`);
continue;
}
// Normalize content_json based on lesson_content_type
let normalizedContentJson = lessonData.content_json;
if (lessonData.lesson_content_type === 'vocabulary' && lessonData.content_json) {
// Convert vocabulary array to words array for consistency
if (lessonData.content_json.vocabulary && Array.isArray(lessonData.content_json.vocabulary)) {
normalizedContentJson = {
...lessonData.content_json,
type: 'vocabulary',
words: lessonData.content_json.vocabulary
};
}
} else if (lessonData.lesson_content_type === 'grammar' && lessonData.content_json) {
// Normalize grammar content
if (lessonData.content_json.grammar) {
normalizedContentJson = {
...lessonData.content_json,
type: 'grammar',
sentences: Array.isArray(lessonData.content_json.grammar)
? lessonData.content_json.grammar
: [lessonData.content_json.grammar]
};
}
} else if (lessonData.lesson_content_type === 'phonics' && lessonData.content_json) {
// Normalize phonics content
normalizedContentJson = {
...lessonData.content_json,
type: 'phonics',
phonics_rules: lessonData.content_json.letters && lessonData.content_json.sounds ? [
{
letters: lessonData.content_json.letters,
sounds: lessonData.content_json.sounds,
words: lessonData.content_json.vocabulary || []
}
] : []
};
}
// Create lesson
const lesson = await Lesson.create({
chapter_id: chapter.id,
lesson_number: lessonData.lesson_number,
lesson_title: lessonData.lesson_title,
lesson_type: lessonData.lesson_type,
lesson_description: lessonData.lesson_description,
lesson_content_type: lessonData.lesson_content_type,
content_json: normalizedContentJson,
content_url: lessonData.content_url || null,
content_type: lessonData.content_type || null,
is_published: true,
is_free: false,
display_order: lessonData.lesson_number
}, { transaction });
stats.lessons++;
console.log(` ✅ Created Lesson ${lessonData.lesson_number}: ${lessonData.lesson_title}`);
} catch (error) {
stats.errors.push(`Failed to create lesson ${lessonData.lesson_number} in ${file}: ${error.message}`);
console.log(` ❌ Error creating lesson ${lessonData.lesson_number}: ${error.message}`);
}
}
}
console.log(''); // Empty line between grades
}
await transaction.commit();
// Print summary
console.log('\n' + '='.repeat(60));
console.log('📊 IMPORT SUMMARY');
console.log('='.repeat(60));
console.log(`✅ Subjects created: ${stats.subjects}`);
console.log(`✅ Chapters created: ${stats.chapters}`);
console.log(`✅ Lessons created: ${stats.lessons}`);
if (stats.errors.length > 0) {
console.log(`\n❌ Errors encountered: ${stats.errors.length}`);
stats.errors.forEach((err, i) => {
console.log(` ${i + 1}. ${err}`);
});
} else {
console.log('\n🎉 No errors!');
}
console.log('='.repeat(60));
console.log('\n✨ Import completed successfully!\n');
} catch (error) {
await transaction.rollback();
console.error('\n❌ FATAL ERROR during import:', error);
throw error;
}
}
// Run import if called directly
if (require.main === module) {
importMoveUpData()
.then(() => {
console.log('✅ Script completed');
process.exit(0);
})
.catch(error => {
console.error('❌ Script failed:', error);
process.exit(1);
});
}
module.exports = { importMoveUpData };