update
This commit is contained in:
1240
GRAMMAR_GUIDE.md
Normal file
1240
GRAMMAR_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
1539
LEARNING_CONTENT_GUIDE.md
Normal file
1539
LEARNING_CONTENT_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
851
STORY_GUIDE.md
Normal file
851
STORY_GUIDE.md
Normal 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
1577
VOCAB_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
9
app.js
9
app.js
@@ -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
|
||||
|
||||
948
controllers/grammarController.js
Normal file
948
controllers/grammarController.js
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
858
controllers/learningContentController.js
Normal file
858
controllers/learningContentController.js
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
642
controllers/storyController.js
Normal file
642
controllers/storyController.js
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
1146
controllers/vocabController.js
Normal file
1146
controllers/vocabController.js
Normal file
File diff suppressed because it is too large
Load Diff
108
data/moveup/g1/unit4.json
Normal file
108
data/moveup/g1/unit4.json
Normal 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
116
data/moveup/g1/unit5.json
Normal 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
106
data/moveup/g1/unit6.json
Normal 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
105
data/moveup/g2/unit4.json
Normal 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
100
data/moveup/g2/unit5.json
Normal 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
101
data/moveup/g2/unit6.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
39
data/moveup/g3/unit10.json
Normal file
39
data/moveup/g3/unit10.json
Normal 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"
|
||||
}
|
||||
]
|
||||
41
data/moveup/g3/unit11.json
Normal file
41
data/moveup/g3/unit11.json
Normal 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"
|
||||
}
|
||||
]
|
||||
42
data/moveup/g3/unit12.json
Normal file
42
data/moveup/g3/unit12.json
Normal 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
42
data/moveup/g3/unit7.json
Normal 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
42
data/moveup/g3/unit8.json
Normal 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
42
data/moveup/g3/unit9.json
Normal 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"
|
||||
}
|
||||
]
|
||||
50
data/moveup/g4/unit10.json
Normal file
50
data/moveup/g4/unit10.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
49
data/moveup/g4/unit11.json
Normal file
49
data/moveup/g4/unit11.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
49
data/moveup/g4/unit12.json
Normal file
49
data/moveup/g4/unit12.json
Normal 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
49
data/moveup/g4/unit7.json
Normal 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
50
data/moveup/g4/unit8.json
Normal 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
49
data/moveup/g4/unit9.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
64
data/moveup/g5/unit10.json
Normal file
64
data/moveup/g5/unit10.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
64
data/moveup/g5/unit11.json
Normal file
64
data/moveup/g5/unit11.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
64
data/moveup/g5/unit12.json
Normal file
64
data/moveup/g5/unit12.json
Normal 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
66
data/moveup/g5/unit7.json
Normal 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
64
data/moveup/g5/unit8.json
Normal 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
64
data/moveup/g5/unit9.json
Normal 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
84
models/Grammar.js
Normal 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
56
models/GrammarMapping.js
Normal 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;
|
||||
71
models/GrammarMediaStory.js
Normal file
71
models/GrammarMediaStory.js
Normal 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;
|
||||
@@ -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
81
models/Story.js
Normal 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
90
models/Vocab.js
Normal 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
78
models/VocabForm.js
Normal 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
72
models/VocabMapping.js
Normal 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
65
models/VocabRelation.js
Normal 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;
|
||||
@@ -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
433
routes/grammarRoutes.js
Normal 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;
|
||||
273
routes/learningContentRoutes.js
Normal file
273
routes/learningContentRoutes.js
Normal 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
333
routes/storyRoutes.js
Normal 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
337
routes/vocabRoutes.js
Normal 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;
|
||||
18
scripts/add-lesson-content-type-column.js
Normal file
18
scripts/add-lesson-content-type-column.js
Normal 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();
|
||||
237
scripts/import-moveup-data.js
Normal file
237
scripts/import-moveup-data.js
Normal 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 };
|
||||
Reference in New Issue
Block a user