From 2c7b4675a78dc93c039dc38daf16c2aba2d957e0 Mon Sep 17 00:00:00 2001 From: silverpro89 Date: Mon, 26 Jan 2026 20:23:08 +0700 Subject: [PATCH] update --- GRAMMAR_GUIDE.md | 1240 ++++++++++++++++ LEARNING_CONTENT_GUIDE.md | 1539 ++++++++++++++++++++ STORY_GUIDE.md | 851 +++++++++++ VOCAB_GUIDE.md | 1577 +++++++++++++++++++++ app.js | 9 + controllers/grammarController.js | 948 +++++++++++++ controllers/learningContentController.js | 858 +++++++++++ controllers/storyController.js | 642 +++++++++ controllers/vocabController.js | 1146 +++++++++++++++ data/moveup/g1/unit4.json | 108 ++ data/moveup/g1/unit5.json | 116 ++ data/moveup/g1/unit6.json | 106 ++ data/moveup/g2/unit4.json | 105 ++ data/moveup/g2/unit5.json | 100 ++ data/moveup/g2/unit6.json | 101 ++ data/moveup/g3/unit10.json | 39 + data/moveup/g3/unit11.json | 41 + data/moveup/g3/unit12.json | 42 + data/moveup/g3/unit7.json | 42 + data/moveup/g3/unit8.json | 42 + data/moveup/g3/unit9.json | 42 + data/moveup/g4/unit10.json | 50 + data/moveup/g4/unit11.json | 49 + data/moveup/g4/unit12.json | 49 + data/moveup/g4/unit7.json | 49 + data/moveup/g4/unit8.json | 50 + data/moveup/g4/unit9.json | 49 + data/moveup/g5/unit10.json | 64 + data/moveup/g5/unit11.json | 64 + data/moveup/g5/unit12.json | 64 + data/moveup/g5/unit7.json | 66 + data/moveup/g5/unit8.json | 64 + data/moveup/g5/unit9.json | 64 + models/Grammar.js | 84 ++ models/GrammarMapping.js | 56 + models/GrammarMediaStory.js | 71 + models/Lesson.js | 14 +- models/Story.js | 81 ++ models/Vocab.js | 90 ++ models/VocabForm.js | 78 + models/VocabMapping.js | 72 + models/VocabRelation.js | 65 + models/index.js | 51 + routes/grammarRoutes.js | 433 ++++++ routes/learningContentRoutes.js | 273 ++++ routes/storyRoutes.js | 333 +++++ routes/vocabRoutes.js | 337 +++++ scripts/add-lesson-content-type-column.js | 18 + scripts/import-moveup-data.js | 237 ++++ 49 files changed, 12668 insertions(+), 1 deletion(-) create mode 100644 GRAMMAR_GUIDE.md create mode 100644 LEARNING_CONTENT_GUIDE.md create mode 100644 STORY_GUIDE.md create mode 100644 VOCAB_GUIDE.md create mode 100644 controllers/grammarController.js create mode 100644 controllers/learningContentController.js create mode 100644 controllers/storyController.js create mode 100644 controllers/vocabController.js create mode 100644 data/moveup/g1/unit4.json create mode 100644 data/moveup/g1/unit5.json create mode 100644 data/moveup/g1/unit6.json create mode 100644 data/moveup/g2/unit4.json create mode 100644 data/moveup/g2/unit5.json create mode 100644 data/moveup/g2/unit6.json create mode 100644 data/moveup/g3/unit10.json create mode 100644 data/moveup/g3/unit11.json create mode 100644 data/moveup/g3/unit12.json create mode 100644 data/moveup/g3/unit7.json create mode 100644 data/moveup/g3/unit8.json create mode 100644 data/moveup/g3/unit9.json create mode 100644 data/moveup/g4/unit10.json create mode 100644 data/moveup/g4/unit11.json create mode 100644 data/moveup/g4/unit12.json create mode 100644 data/moveup/g4/unit7.json create mode 100644 data/moveup/g4/unit8.json create mode 100644 data/moveup/g4/unit9.json create mode 100644 data/moveup/g5/unit10.json create mode 100644 data/moveup/g5/unit11.json create mode 100644 data/moveup/g5/unit12.json create mode 100644 data/moveup/g5/unit7.json create mode 100644 data/moveup/g5/unit8.json create mode 100644 data/moveup/g5/unit9.json create mode 100644 models/Grammar.js create mode 100644 models/GrammarMapping.js create mode 100644 models/GrammarMediaStory.js create mode 100644 models/Story.js create mode 100644 models/Vocab.js create mode 100644 models/VocabForm.js create mode 100644 models/VocabMapping.js create mode 100644 models/VocabRelation.js create mode 100644 routes/grammarRoutes.js create mode 100644 routes/learningContentRoutes.js create mode 100644 routes/storyRoutes.js create mode 100644 routes/vocabRoutes.js create mode 100644 scripts/add-lesson-content-type-column.js create mode 100644 scripts/import-moveup-data.js diff --git a/GRAMMAR_GUIDE.md b/GRAMMAR_GUIDE.md new file mode 100644 index 0000000..bea00ac --- /dev/null +++ b/GRAMMAR_GUIDE.md @@ -0,0 +1,1240 @@ +# Grammar System Guide + +**Version:** 1.0.0 +**Last Updated:** 2026-01-26 +**Purpose:** Complete guide for managing grammar rules in the SENA Language Learning System + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Database Structure](#database-structure) +3. [API Endpoints](#api-endpoints) +4. [Grammar Engine Concepts](#grammar-engine-concepts) +5. [Pattern Logic System](#pattern-logic-system) +6. [Sentence Generation Algorithm](#sentence-generation-algorithm) +7. [Use Cases & Examples](#use-cases--examples) +8. [Database Queries](#database-queries) +9. [Validation Rules](#validation-rules) +10. [AI Integration Guide](#ai-integration-guide) + +--- + +## Overview + +The Grammar System is the **brain** that controls how vocabulary is mixed into sentences and stories. It provides: + +- **Sentence Templates**: Define grammatical structures (e.g., "S + V + O") +- **Pattern Logic**: Smart rules to pick words from Vocabulary table based on roles and semantics +- **Curriculum Mapping**: Track where each grammar appears in textbooks +- **Media Stories**: Attach stories/videos that demonstrate the grammar rule +- **Automatic Sentence Generation**: Generate grammatically correct and semantically meaningful sentences + +### Key Features + +✅ **Role-Based Word Selection** - Pick words by syntax role (subject, verb, object, etc.) +✅ **Semantic Filtering** - Ensure semantic compatibility (e.g., "humans eat food", not "food eats humans") +✅ **Grammar Agreement** - Auto-select correct be verb (am/is/are) based on subject +✅ **Article Selection** - Choose a/an based on phonetic sound +✅ **Form Selection** - Pick correct word form (v1, v_ing, v2, n_singular, etc.) +✅ **Dependency Management** - Link slots for grammar agreement +✅ **Optional Slots** - Support sentence variations + +--- + +## Database Structure + +### Table: `grammars` + +Main table storing grammar rule definitions. + +| Column | Type | Required | Description | +|--------|------|----------|-------------| +| `id` | INTEGER | Auto | Primary key | +| `grammar_code` | STRING(100) | ✓ | Unique identifier (e.g., "gram-001-present-cont") | +| `title` | STRING(200) | ✓ | Grammar name (e.g., "Present Continuous") | +| `translation` | STRING(200) | - | Vietnamese translation | +| `structure` | JSON | ✓ | Formula + pattern_logic (see below) | +| `instructions` | JSON | - | { vi: string, hint: string } | +| `difficulty_score` | INTEGER | - | 1-10 (default: 1) | +| `category` | STRING(100) | - | e.g., "Tenses", "Modal Verbs" | +| `tags` | JSON | - | Array of tags | +| `is_active` | BOOLEAN | ✓ | Soft delete flag (default: true) | +| `createdAt` | TIMESTAMP | Auto | Creation timestamp | +| `updatedAt` | TIMESTAMP | Auto | Last update timestamp | + +#### `structure` Field Format + +```json +{ + "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": "O_01", + "role": "is_object", + "semantic_match": "V_01" + } + ] +} +``` + +### Table: `grammar_mappings` + +Curriculum mapping - tracks where grammar appears in textbooks. + +| Column | Type | Required | Description | +|--------|------|----------|-------------| +| `id` | INTEGER | Auto | Primary key | +| `grammar_id` | INTEGER | ✓ | FK to grammars.id (cascade delete) | +| `book_id` | STRING(100) | ✓ | Book identifier (e.g., "global-success-2") | +| `grade` | INTEGER | ✓ | Grade level | +| `unit` | INTEGER | - | Unit number | +| `lesson` | INTEGER | - | Lesson number | +| `context_note` | TEXT | - | Additional context | + +### Table: `grammar_media_stories` + +Media content demonstrating the grammar rule. + +| Column | Type | Required | Description | +|--------|------|----------|-------------| +| `id` | INTEGER | Auto | Primary key | +| `grammar_id` | INTEGER | ✓ | FK to grammars.id (cascade delete) | +| `story_id` | STRING(100) | ✓ | Unique story identifier | +| `title` | STRING(200) | ✓ | Story title | +| `type` | ENUM | ✓ | 'story', 'video', 'animation', 'audio' | +| `url` | STRING(500) | ✓ | Single URL to complete media file | +| `thumbnail` | STRING(500) | - | Thumbnail image URL | +| `description` | TEXT | - | Story description | +| `duration_seconds` | INTEGER | - | Media duration | +| `min_grade` | INTEGER | - | Minimum grade level | + +--- + +## API Endpoints + +Base URL: `/api/grammar` + +### 1. Create Grammar Rule + +**POST** `/api/grammar` + +**Headers:** +``` +Authorization: Bearer +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "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": "O_01", + "role": "is_object", + "semantic_match": "V_01" + } + ] + }, + "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 + } + ] +} +``` + +**Response (201):** +```json +{ + "success": true, + "message": "Grammar rule created successfully", + "data": { + "id": 1, + "grammar_code": "gram-001-present-cont", + "title": "Present Continuous", + "mappings": [...], + "mediaStories": [...] + } +} +``` + +### 2. Get All Grammar Rules + +**GET** `/api/grammar?page=1&limit=20&category=Tenses&grade=2&include_media=true` + +**Query Parameters:** +- `page` (integer, default: 1) +- `limit` (integer, default: 20) +- `category` (string) - Filter by category +- `grade` (integer) - Filter by grade level +- `book_id` (string) - Filter by book ID +- `difficulty_min` (integer) - Minimum difficulty +- `difficulty_max` (integer) - Maximum difficulty +- `search` (string) - Search in title/translation/code +- `include_media` ('true'/'false', default: 'false') + +**Response (200):** +```json +{ + "success": true, + "data": [...], + "pagination": { + "total": 50, + "page": 1, + "limit": 20, + "totalPages": 3 + } +} +``` + +### 3. Get Grammar by ID or Code + +**GET** `/api/grammar/:id` + +`:id` can be numeric ID or grammar_code string. + +**Examples:** +- `/api/grammar/1` - Get by ID +- `/api/grammar/gram-001-present-cont` - Get by code + +**Response (200):** +```json +{ + "success": true, + "data": { + "id": 1, + "grammar_code": "gram-001-present-cont", + "title": "Present Continuous", + "structure": {...}, + "mappings": [...], + "mediaStories": [...] + } +} +``` + +### 4. Update Grammar Rule + +**PUT** `/api/grammar/:id` + +**Request Body:** (partial update) +```json +{ + "translation": "Thì hiện tại tiếp diễn (cập nhật)", + "difficulty_score": 3, + "tags": ["present", "continuous", "action", "updated"] +} +``` + +**Response (200):** +```json +{ + "success": true, + "message": "Grammar rule updated successfully", + "data": {...} +} +``` + +### 5. Delete Grammar Rule + +**DELETE** `/api/grammar/:id` + +Performs soft delete (sets `is_active = false`). + +**Response (200):** +```json +{ + "success": true, + "message": "Grammar rule deleted successfully" +} +``` + +### 6. Get Grammars by Curriculum + +**GET** `/api/grammar/curriculum?book_id=global-success-2&grade=2&unit=5&lesson=1` + +**Query Parameters:** +- `book_id` (string, required if grade not provided) +- `grade` (integer, required if book_id not provided) +- `unit` (integer, optional) +- `lesson` (integer, optional) + +**Response (200):** +```json +{ + "success": true, + "data": [...], + "count": 5 +} +``` + +### 7. Get Grammar Statistics + +**GET** `/api/grammar/stats` + +**Response (200):** +```json +{ + "success": true, + "data": { + "total": 50, + "by_category": [ + { "category": "Tenses", "count": 20 }, + { "category": "Modal Verbs", "count": 10 } + ], + "by_difficulty": [ + { "difficulty_score": 1, "count": 15 }, + { "difficulty_score": 2, "count": 20 } + ], + "by_grade": [ + { "grade": 1, "count": 10 }, + { "grade": 2, "count": 15 } + ] + } +} +``` + +### 8. Get Grammar Guide (for AI) + +**GET** `/api/grammar/guide` + +Returns comprehensive guide in JSON format for AI agents to understand how to create grammar rules. + +**Response (200):** +```json +{ + "success": true, + "data": { + "guide_version": "1.0.0", + "data_structure": {...}, + "pattern_logic_roles": {...}, + "semantic_filters": {...}, + "form_keys_reference": {...}, + "rules": {...}, + "examples": {...}, + "validation_checklist": [...], + "common_mistakes": [...], + "ai_tips": {...} + } +} +``` + +--- + +## Grammar Engine Concepts + +### 1. Pattern Logic System + +Pattern logic defines **slots** that pick words from the Vocabulary table based on **roles** and **filters**. + +#### Slot Properties + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `slot_id` | string | ✓ | Unique slot identifier (e.g., "S_01", "V_01") | +| `role` | string | ✓ | Word role from vocab.syntax (see roles below) | +| `semantic_filter` | array | - | Filter by semantic types (e.g., ["human", "animal"]) | +| `use_form` | string | - | Which form to use (v1, v_ing, v2, n_singular, etc.) | +| `dependency` | string | - | Slot ID this depends on for agreement | +| `is_optional` | boolean | - | Can be skipped (default: false) | +| `position` | string | - | Placement hint: 'start', 'middle', 'end' | +| `semantic_match` | string | - | Match with another slot for compatibility | + +#### Available Roles + +Roles query the `vocab.syntax` JSON field: + +| Role | Description | Example Words | +|------|-------------|---------------| +| `is_subject` | Can be sentence subject | I, cat, teacher, apple | +| `is_verb` | Action or state verb | eat, run, sleep, love | +| `is_object` | Can be sentence object | apple, book, water | +| `is_be` | Be verb | am, is, are | +| `is_adj` | Adjective | happy, big, red | +| `is_adv` | Adverb | quickly, slowly, happily | +| `is_article` | Article | a, an | +| `is_pronoun` | Pronoun | I, you, he, she, it | +| `is_preposition` | Preposition | in, on, at, under | + +### 2. Semantic Filters + +Filter words by semantic types from `vocab.semantics`: + +**Common Semantic Types:** +- `human` - People, person, teacher +- `animal` - Cat, dog, bird +- `object` - Book, pen, table +- `food` - Apple, rice, bread +- `plant` - Tree, flower, grass +- `place` - School, park, home +- `abstract` - Love, idea, freedom +- `emotion` - Happy, sad, angry +- `action` - Eat, run, jump +- `state` - Be, have, exist + +**Example:** +```json +{ + "slot_id": "S_01", + "role": "is_subject", + "semantic_filter": ["human", "animal"] +} +``` +→ Only selects subjects that are humans or animals. + +### 3. Form Selection + +Use `use_form` to pick specific word variations from `VocabForm` table: + +**Verb Forms:** +- `v1` - Base form (eat, go) +- `v_s_es` - Third person singular (eats, goes) +- `v_ing` - Present participle (eating, going) +- `v2` - Past simple (ate, went) +- `v3` - Past participle (eaten, gone) + +**Noun Forms:** +- `n_singular` - Singular (cat, book) +- `n_plural` - Plural (cats, books) + +**Adjective Forms:** +- `adj_base` - Base (happy, big) +- `adj_comparative` - Comparative (happier, bigger) +- `adj_superlative` - Superlative (happiest, biggest) + +**Adverb Forms:** +- `adv_manner` - Manner (quickly, slowly) +- `adv_frequency` - Frequency (always, never) +- `adv_time` - Time (yesterday, today) + +**Example:** +```json +{ + "slot_id": "V_01", + "role": "is_verb", + "use_form": "v_ing" +} +``` +→ Picks the V-ing form (e.g., "eating" from "eat"). + +### 4. Dependency Rules + +Link slots for grammar agreement using `dependency` field. + +#### Be Verb Agreement + +```json +{ + "slot_id": "BE_01", + "role": "is_be", + "dependency": "S_01" +} +``` + +**Logic:** +- If `S_01` is "I" → use "am" +- If `S_01` is singular (he, she, it, cat) → use "is" +- If `S_01` is plural (we, they, cats) → use "are" + +#### Article Selection + +```json +{ + "slot_id": "ART_01", + "role": "is_article", + "dependency": "O_01" +} +``` + +**Logic:** +- Check `O_01` phonetic from VocabForm +- If starts with vowel sound (/æ/, /e/, /ɪ/, /ɒ/, /ʌ/) → use "an" +- Otherwise → use "a" + +**Example:** +- "an apple" (/ˈæp.əl/ - vowel sound) +- "a book" (/bʊk/ - consonant sound) + +#### Semantic Matching + +```json +{ + "slot_id": "O_01", + "role": "is_object", + "semantic_match": "V_01" +} +``` + +**Logic:** +- Check `V_01.semantics.can_take_object_type` +- Match with `O_01.semantics.word_type` +- Only select objects the verb can take + +**Example:** +- Verb "eat" can take object types: ["food", "plant"] +- Object "apple" has word_type: "food" +- ✅ Valid match: "eat an apple" +- ❌ Invalid: "eat a table" (table is "object", not "food") + +--- + +## Sentence Generation Algorithm + +### Step-by-Step Process + +1. **Load Grammar Rule** + ```javascript + const grammar = await Grammar.findOne({ + where: { grammar_code: 'gram-001-present-cont' }, + include: ['mappings', 'mediaStories'] + }); + ``` + +2. **Parse Pattern Logic** + ```javascript + const pattern_logic = grammar.structure.pattern_logic; + ``` + +3. **Loop Through Slots (in order)** + ```javascript + for (let slot of pattern_logic) { + // Step 4-8 for each slot + } + ``` + +4. **Query Vocab by Role** + ```sql + SELECT * FROM vocab + WHERE syntax->>'is_verb' = 'true' + AND is_active = true; + ``` + +5. **Apply Semantic Filter** + ```sql + WHERE semantics->'can_be_subject_type' @> '["human"]'::jsonb + ``` + +6. **Fetch Specific Form** + ```sql + SELECT vf.text, vf.phonetic, vf.audio + FROM vocab_forms vf + WHERE vf.vocab_id = ? AND vf.form_key = 'v_ing' + ``` + +7. **Apply Dependency Rules** + - If `dependency` = "S_01", apply be verb agreement + - If `dependency` = "O_01", apply article selection + - If `semantic_match` = "V_01", check semantic compatibility + +8. **Random Selection** + ```javascript + const randomWord = words[Math.floor(Math.random() * words.length)]; + ``` + +9. **Handle Optional Slots** + ```javascript + if (slot.is_optional && Math.random() > 0.5) { + continue; // Skip this slot + } + ``` + +10. **Concatenate Result** + ```javascript + sentence = selectedWords.map(w => w.text).join(' '); + // "The cat is eating an apple quickly" + ``` + +### Example Generation + +**Grammar:** Present Continuous +**Formula:** `S + am/is/are + V-ing + (a/an) + O + Adv` + +**Pattern Logic:** +1. `S_01`: is_subject, semantic_filter: ["animal"] → **"cat"** +2. `BE_01`: is_be, dependency: S_01 → **"is"** (singular) +3. `V_01`: is_verb, use_form: v_ing, semantic_filter: ["action"] → **"eating"** +4. `ART_01`: is_article, dependency: O_01 → **"an"** (apple starts with vowel) +5. `O_01`: is_object, semantic_match: V_01 → **"apple"** (food) +6. `ADV_01`: is_adv, is_optional: true → **"quickly"** + +**Generated Sentence:** +``` +"The cat is eating an apple quickly." +``` + +**With Audio:** +```json +{ + "text": "The cat is eating an apple quickly.", + "audio_segments": [ + { "word": "The", "audio": "https://cdn.sena.tech/audio/the.mp3" }, + { "word": "cat", "audio": "https://cdn.sena.tech/audio/cat_n_singular.mp3" }, + { "word": "is", "audio": "https://cdn.sena.tech/audio/is.mp3" }, + { "word": "eating", "audio": "https://cdn.sena.tech/audio/eating_v_ing.mp3" }, + { "word": "an", "audio": "https://cdn.sena.tech/audio/an.mp3" }, + { "word": "apple", "audio": "https://cdn.sena.tech/audio/apple_n_singular.mp3" }, + { "word": "quickly", "audio": "https://cdn.sena.tech/audio/quickly_adv_manner.mp3" } + ] +} +``` + +--- + +## Use Cases & Examples + +### Use Case 1: Present Continuous Tense + +**Grammar Rule:** +```json +{ + "grammar_code": "gram-001-present-cont", + "title": "Present Continuous", + "structure": { + "formula": "S + am/is/are + V-ing + O", + "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" }, + { "slot_id": "O_01", "role": "is_object", "semantic_match": "V_01" } + ] + } +} +``` + +**Generated Sentences:** +- "I am eating an apple." +- "The cat is drinking milk." +- "They are playing football." + +### Use Case 2: Yes/No Questions + +**Grammar Rule:** +```json +{ + "grammar_code": "gram-010-yes-no-question", + "title": "Yes/No Questions with 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" }, + { "slot_id": "ADJ_01", "role": "is_adj" } + ] + } +} +``` + +**Generated Sentences:** +- "Is the cat happy?" +- "Are you hungry?" +- "Am I late?" + +### Use Case 3: Modal Verbs (Can) + +**Grammar Rule:** +```json +{ + "grammar_code": "gram-020-modal-can", + "title": "Modal Verb Can", + "structure": { + "formula": "S + can + V1 + O", + "pattern_logic": [ + { "slot_id": "S_01", "role": "is_subject", "semantic_filter": ["human"] }, + { "slot_id": "V_01", "role": "is_verb", "use_form": "v1" }, + { "slot_id": "O_01", "role": "is_object", "semantic_match": "V_01", "is_optional": true } + ] + } +} +``` + +**Generated Sentences:** +- "I can swim." +- "She can play the piano." +- "They can speak English." + +### Use Case 4: Past Simple Tense + +**Grammar Rule:** +```json +{ + "grammar_code": "gram-030-past-simple", + "title": "Past Simple", + "structure": { + "formula": "S + V2 + O + Adv", + "pattern_logic": [ + { "slot_id": "S_01", "role": "is_subject" }, + { "slot_id": "V_01", "role": "is_verb", "use_form": "v2" }, + { "slot_id": "O_01", "role": "is_object", "semantic_match": "V_01" }, + { "slot_id": "ADV_01", "role": "is_adv", "is_optional": true } + ] + } +} +``` + +**Generated Sentences:** +- "I ate an apple yesterday." +- "The cat slept all day." +- "They played football." + +### Use Case 5: Wh-Questions + +**Grammar Rule:** +```json +{ + "grammar_code": "gram-040-wh-question", + "title": "Wh-Questions (What)", + "structure": { + "formula": "What + am/is/are + S + V-ing?", + "pattern_logic": [ + { "slot_id": "BE_01", "role": "is_be" }, + { "slot_id": "S_01", "role": "is_subject" }, + { "slot_id": "V_01", "role": "is_verb", "use_form": "v_ing" } + ] + } +} +``` + +**Generated Sentences:** +- "What are you doing?" +- "What is the cat eating?" +- "What am I wearing?" + +--- + +## Database Queries + +### Query 1: Find All Grammars for Grade 2 + +```sql +SELECT g.* +FROM grammars g +JOIN grammar_mappings gm ON g.id = gm.grammar_id +WHERE gm.grade = 2 AND g.is_active = true +ORDER BY g.difficulty_score ASC; +``` + +### Query 2: Get Grammars by Category + +```sql +SELECT * FROM grammars +WHERE category = 'Tenses' AND is_active = true +ORDER BY difficulty_score ASC; +``` + +### Query 3: Find Grammars Using Specific Role + +```sql +SELECT * FROM grammars +WHERE structure::text LIKE '%"role":"is_verb"%' +AND is_active = true; +``` + +### Query 4: Get All Media Stories for a Grammar + +```sql +SELECT gms.* +FROM grammar_media_stories gms +JOIN grammars g ON gms.grammar_id = g.id +WHERE g.grammar_code = 'gram-001-present-cont'; +``` + +### Query 5: Count Grammars by Difficulty + +```sql +SELECT difficulty_score, COUNT(*) as count +FROM grammars +WHERE is_active = true +GROUP BY difficulty_score +ORDER BY difficulty_score ASC; +``` + +### Query 6: Find Grammars with Optional Slots + +```sql +SELECT * FROM grammars +WHERE structure::text LIKE '%"is_optional":true%' +AND is_active = true; +``` + +### Query 7: Get Curriculum Coverage + +```sql +SELECT gm.book_id, gm.grade, COUNT(DISTINCT g.id) as grammar_count +FROM grammar_mappings gm +JOIN grammars g ON gm.grammar_id = g.id +WHERE g.is_active = true +GROUP BY gm.book_id, gm.grade +ORDER BY gm.book_id, gm.grade; +``` + +--- + +## Validation Rules + +### 1. Grammar Code Format + +**Rule:** `gram-{3-digit-sequence}-{kebab-case-identifier}` + +**Valid:** +- `gram-001-present-cont` +- `gram-015-modal-can` +- `gram-100-passive-voice` + +**Invalid:** +- `gram-1-test` (not 3 digits) +- `GRAM-001-TEST` (uppercase) +- `present-continuous` (no prefix) + +### 2. Structure Requirements + +**Required:** +- `formula` (string) - Must be present +- `pattern_logic` (array) - Must have at least 1 slot + +**Example:** +```json +{ + "formula": "S + V + O", + "pattern_logic": [ + { "slot_id": "S_01", "role": "is_subject" } + ] +} +``` + +### 3. Slot Validation + +**Required Properties:** +- `slot_id` - Unique identifier +- `role` - Must be valid role (see Available Roles) + +**Optional Properties:** +- `semantic_filter` - Array of semantic types +- `use_form` - Must be valid form key +- `dependency` - Must reference existing slot_id +- `is_optional` - Boolean +- `position` - 'start', 'middle', 'end' + +### 4. Dependency Validation + +**Rule:** `dependency` must reference a slot_id that appears **before** the current slot. + +**Valid:** +```json +[ + { "slot_id": "S_01", "role": "is_subject" }, + { "slot_id": "BE_01", "role": "is_be", "dependency": "S_01" } +] +``` + +**Invalid:** +```json +[ + { "slot_id": "BE_01", "role": "is_be", "dependency": "S_01" }, + { "slot_id": "S_01", "role": "is_subject" } +] +``` + +### 5. Form Key Validation + +**Rule:** `use_form` must match valid form keys: + +- Verbs: `v1`, `v_s_es`, `v_ing`, `v2`, `v3` +- Nouns: `n_singular`, `n_plural` +- Adjectives: `adj_base`, `adj_comparative`, `adj_superlative` +- Adverbs: `adv_manner`, `adv_frequency`, `adv_time` + +### 6. Semantic Filter Validation + +**Rule:** Semantic types must be consistent with vocab semantics. + +**Common Types:** +- Subject types: `human`, `animal`, `object` +- Object types: `food`, `plant`, `tool`, `container` +- Action types: `action`, `state`, `emotion` + +### 7. Media Story Validation + +**Required Properties:** +- `story_id` - Unique identifier +- `title` - Story title +- `type` - Must be 'story', 'video', 'animation', or 'audio' +- `url` - Valid URL + +**Optional Properties:** +- `thumbnail` - Image URL +- `description` - Text description +- `duration_seconds` - Integer +- `min_grade` - Integer + +--- + +## AI Integration Guide + +### For AI Agents Creating Grammar Rules + +#### Step 1: Analyze Curriculum + +1. Identify grammar topic (e.g., "Present Continuous") +2. Determine difficulty level (1-10) +3. Find curriculum mapping (book, grade, unit, lesson) + +#### Step 2: Define Formula + +Create human-readable formula: +``` +S + am/is/are + V-ing + (a/an) + O + Adv +``` + +**Tips:** +- Use S for Subject, V for Verb, O for Object +- Show alternatives with `/` (am/is/are) +- Mark optional with `()` (Adv) + +#### Step 3: Design Pattern Logic + +For each word in formula, create a slot: + +```json +{ + "slot_id": "S_01", + "role": "is_subject", + "semantic_filter": ["human", "animal"] +} +``` + +**Checklist:** +- ✓ Unique `slot_id` (S_01, V_01, O_01, etc.) +- ✓ Valid `role` from pattern_logic_roles +- ✓ Semantic filter matches curriculum level +- ✓ Specify `use_form` for verbs/nouns +- ✓ Add `dependency` for grammar agreement +- ✓ Mark `is_optional` for sentence variations + +#### Step 4: Add Dependencies + +Link slots for grammar rules: + +**Be Verb Agreement:** +```json +{ + "slot_id": "BE_01", + "role": "is_be", + "dependency": "S_01" +} +``` + +**Article Selection:** +```json +{ + "slot_id": "ART_01", + "role": "is_article", + "dependency": "O_01" +} +``` + +**Semantic Matching:** +```json +{ + "slot_id": "O_01", + "role": "is_object", + "semantic_match": "V_01" +} +``` + +#### Step 5: Create Instructions + +```json +{ + "vi": "Dùng để nói về hành động đang diễn ra.", + "hint": "Cấu trúc: Be + V-ing" +} +``` + +#### Step 6: Add Mappings + +```json +{ + "mappings": [ + { + "book_id": "global-success-2", + "grade": 2, + "unit": 5, + "lesson": 1 + } + ] +} +``` + +#### Step 7: Attach Media Stories + +```json +{ + "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." + } + ] +} +``` + +#### Step 8: Validate & Submit + +**Pre-Submit Checklist:** +1. ✓ `grammar_code` follows format +2. ✓ `structure.formula` is clear +3. ✓ `pattern_logic` has at least 1 slot +4. ✓ All roles are valid +5. ✓ `use_form` values are correct +6. ✓ Dependencies reference valid slots +7. ✓ Semantic filters match vocab +8. ✓ Media URLs are accessible +9. ✓ `difficulty_score` is 1-10 +10. ✓ At least 1 mapping exists + +**Submit via API:** +```bash +POST /api/grammar +Content-Type: application/json +Authorization: Bearer + +{...grammar data...} +``` + +#### Common Mistakes to Avoid + +1. **Invalid Role** + - ❌ `"role": "is_noun"` + - ✅ `"role": "is_object"` + +2. **Missing Formula** + - ❌ `"structure": { "pattern_logic": [...] }` + - ✅ `"structure": { "formula": "S + V + O", "pattern_logic": [...] }` + +3. **Invalid Dependency** + - ❌ `"dependency": "INVALID_SLOT"` + - ✅ `"dependency": "S_01"` + +4. **Wrong Form Key** + - ❌ `{ "role": "is_verb", "use_form": "n_singular" }` + - ✅ `{ "role": "is_verb", "use_form": "v_ing" }` + +5. **Semantic Mismatch** + - ❌ `{ "role": "is_verb", "semantic_filter": ["object"] }` + - ✅ `{ "role": "is_verb", "semantic_filter": ["action"] }` + +#### AI Tips + +- **Efficiency:** Create grammars in order of difficulty (easy → hard) +- **Accuracy:** Validate pattern_logic against actual vocab entries +- **Completeness:** Include mappings and media stories +- **Testing:** Test sentence generation with sample vocab +- **Documentation:** Use clear formulas and instructions + +--- + +## API Integration Example (JavaScript) + +### Create Grammar Rule + +```javascript +const axios = require('axios'); + +async function createGrammar(token) { + try { + const response = await axios.post('http://localhost:3000/api/grammar', { + 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 + } + ] + }, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + console.log('✅ Grammar created:', response.data); + } catch (error) { + console.error('❌ Error:', error.response?.data || error.message); + } +} +``` + +### Get Grammar Guide + +```javascript +async function getGrammarGuide(token) { + try { + const response = await axios.get('http://localhost:3000/api/grammar/guide', { + headers: { 'Authorization': `Bearer ${token}` } + }); + + const guide = response.data.data; + console.log('📖 Grammar Guide Version:', guide.guide_version); + console.log('📊 Pattern Logic Roles:', guide.pattern_logic_roles); + console.log('✅ Validation Checklist:', guide.validation_checklist); + } catch (error) { + console.error('❌ Error:', error.message); + } +} +``` + +--- + +## Summary + +The Grammar System provides a powerful framework for: + +1. **Defining Grammar Rules** - Store templates with formula and pattern logic +2. **Automatic Sentence Generation** - Mix vocabulary intelligently based on roles and semantics +3. **Curriculum Mapping** - Track where each grammar appears in textbooks +4. **Media Integration** - Attach stories/videos for engaging learning +5. **AI-Ready** - Comprehensive guide for automated data entry + +**Key Benefits:** +- ✅ Generates grammatically correct sentences +- ✅ Ensures semantic compatibility +- ✅ Supports all grammar types (tenses, questions, modals, etc.) +- ✅ Provides audio for each word +- ✅ Tracks curriculum coverage +- ✅ AI-friendly for automated content creation + +**Next Steps:** +1. Create vocabulary entries with complete syntax and semantics +2. Define grammar rules for each textbook lesson +3. Implement sentence generation service +4. Integrate with Grammar Engine for real-time sentence creation +5. Add media stories for engaging content + +--- + +**For questions or support, contact the SENA development team.** diff --git a/LEARNING_CONTENT_GUIDE.md b/LEARNING_CONTENT_GUIDE.md new file mode 100644 index 0000000..7fadd7c --- /dev/null +++ b/LEARNING_CONTENT_GUIDE.md @@ -0,0 +1,1539 @@ +# LEARNING CONTENT GUIDE + +**Version:** 1.0.0 +**Last Updated:** 2024 +**Purpose:** Comprehensive guide for AI to understand and create learning content in the Subject → Chapter → Lesson hierarchy + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Content Hierarchy](#content-hierarchy) +3. [Subject Structure](#subject-structure) +4. [Chapter Structure](#chapter-structure) +5. [Lesson Structure](#lesson-structure) +6. [Lesson Content Types](#lesson-content-types) +7. [Exercise Types](#exercise-types) +8. [API Workflow](#api-workflow) +9. [Code Examples](#code-examples) +10. [Validation Checklist](#validation-checklist) +11. [Common Mistakes](#common-mistakes) +12. [AI Integration Tips](#ai-integration-tips) + +--- + +## Overview + +The learning content system is structured in a three-level hierarchy: + +``` +Subject (Course/Curriculum) + └── Chapter (Unit/Topic) + └── Lesson (Individual Learning Activity) +``` + +Each level serves a specific purpose: +- **Subject**: Represents a complete course (e.g., "English Grade 1", "Math Grade 5") +- **Chapter**: Represents a major topic within a subject (e.g., "Unit 1: My Family", "Chapter 3: Fractions") +- **Lesson**: Represents individual learning activities with specific content types + +--- + +## Content Hierarchy + +### Database Schema + +``` +┌─────────────┐ +│ Subject │ +│ (UUID) │ +└──────┬──────┘ + │ 1:N + │ +┌──────▼──────┐ +│ Chapter │ +│ (UUID) │ +└──────┬──────┘ + │ 1:N + │ +┌──────▼──────┐ +│ Lesson │ +│ (UUID) │ +└─────────────┘ +``` + +### Relationships + +- One Subject can have **many Chapters** +- One Chapter can have **many Lessons** +- All IDs are **UUID** (for security against crawlers) +- Foreign keys: `subject_id` in Chapter, `chapter_id` in Lesson + +--- + +## Subject Structure + +### Database Model: `Subject` + +**Table:** `subjects` + +**Required Fields:** + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `id` | UUID | Primary key | `550e8400-e29b-41d4-a716-446655440000` | +| `subject_code` | VARCHAR(20) | Unique subject code | `ENG-G1` | +| `subject_name` | VARCHAR(100) | Subject name (Vietnamese) | `Tiếng Anh Lớp 1` | + +**Optional Fields:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `subject_name_en` | VARCHAR(100) | NULL | English name | +| `description` | TEXT | NULL | Subject description | +| `is_active` | BOOLEAN | true | Active status | +| `is_premium` | BOOLEAN | false | Premium content flag | +| `is_training` | BOOLEAN | false | Training content flag | +| `is_public` | BOOLEAN | false | Public self-learning flag | +| `required_role` | VARCHAR(50) | NULL | Required role to access | + +### Subject Access Control + +The `Subject` model includes access control fields: + +- **`is_premium`**: Content requires subscription +- **`is_training`**: Content for staff training +- **`is_public`**: Available for public self-learning +- **`required_role`**: Specific role required (e.g., "teacher", "admin") + +### Example Subject + +```json +{ + "subject_code": "ENG-G1", + "subject_name": "Tiếng Anh Lớp 1", + "subject_name_en": "English Grade 1", + "description": "Basic English course for Grade 1 students", + "is_active": true, + "is_premium": false, + "is_public": true +} +``` + +--- + +## Chapter Structure + +### Database Model: `Chapter` + +**Table:** `chapters` + +**Required Fields:** + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `id` | UUID | Primary key | `550e8400-e29b-41d4-a716-446655440001` | +| `subject_id` | UUID | Foreign key to Subject | `550e8400-e29b-41d4-a716-446655440000` | +| `chapter_number` | INTEGER | Sequential chapter number | `1` | +| `chapter_title` | VARCHAR(200) | Chapter title | `Unit 1: My Family` | + +**Optional Fields:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `chapter_description` | TEXT | NULL | Chapter description | +| `duration_minutes` | INTEGER | NULL | Estimated duration | +| `is_published` | BOOLEAN | false | Published status | +| `display_order` | INTEGER | NULL | Custom display order | + +### Chapter Numbering + +- **`chapter_number`**: Sequential number (1, 2, 3, ...) +- **`display_order`**: Optional custom order (for reordering without renumbering) + +### Example Chapter + +```json +{ + "subject_id": "550e8400-e29b-41d4-a716-446655440000", + "chapter_number": 1, + "chapter_title": "Unit 1: My Family", + "chapter_description": "Learn about family members and relationships", + "duration_minutes": 120, + "is_published": true, + "display_order": 1 +} +``` + +--- + +## Lesson Structure + +### Database Model: `Lesson` + +**Table:** `lessons` + +**Required Fields:** + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `id` | UUID | Primary key | `550e8400-e29b-41d4-a716-446655440010` | +| `chapter_id` | UUID | Foreign key to Chapter | `550e8400-e29b-41d4-a716-446655440001` | +| `lesson_number` | INTEGER | Sequential lesson number | `1` | +| `lesson_title` | VARCHAR(200) | Lesson title | `Family Vocabulary` | +| `lesson_type` | ENUM | Content delivery type | `json_content` or `url_content` | + +**Optional Fields:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `lesson_description` | TEXT | NULL | Lesson description | +| `lesson_content_type` | ENUM | NULL | Content category: `vocabulary`, `grammar`, `phonics`, `review`, `mixed` | +| `content_json` | JSON | NULL | JSON content structure | +| `content_url` | VARCHAR(500) | NULL | URL for external content | +| `content_type` | VARCHAR(50) | NULL | URL content type: `video`, `audio`, `pdf`, `image`, `interactive` | +| `duration_minutes` | INTEGER | NULL | Estimated duration | +| `is_published` | BOOLEAN | false | Published status | +| `is_free` | BOOLEAN | false | Free trial access | +| `display_order` | INTEGER | 0 | Custom display order | +| `thumbnail_url` | VARCHAR(500) | NULL | Lesson thumbnail | + +### Lesson Types + +#### 1. `lesson_type` (Content Delivery) + +- **`json_content`**: Content is stored in `content_json` field (structured data) +- **`url_content`**: Content is external, referenced by `content_url` + +#### 2. `lesson_content_type` (Content Category) + +- **`vocabulary`**: Vocabulary learning lesson +- **`grammar`**: Grammar rules and practice +- **`phonics`**: Pronunciation and phonics +- **`review`**: Review lesson combining multiple types +- **`mixed`**: Mixed content (custom combination) + +### Validation Rules + +1. If `lesson_type = 'json_content'`, then `content_json` is **required** +2. If `lesson_type = 'url_content'`, then `content_url` is **required** +3. `lesson_content_type` and `content_json.type` should **match** for consistency +4. Lesson numbers should be **sequential** within a chapter + +--- + +## Lesson Content Types + +### 1. Vocabulary Lesson + +**Purpose:** Teaching vocabulary with word lists and exercises + +**`lesson_content_type`:** `vocabulary` + +**`content_json` Structure:** + +```json +{ + "type": "vocabulary", + "words": [ + "mother", + "father", + "sister", + "brother" + ], + "exercises": [ + { + "type": "match", + "question": "Match the words with pictures", + "items": [ + { "word": "mother", "image": "https://..." }, + { "word": "father", "image": "https://..." } + ] + } + ] +} +``` + +**Key Fields:** + +- **`words`**: Array of vocabulary words (strings). **System will automatically lookup these words in the `Vocab` table when rendering the lesson** +- **`exercises`**: Array of practice exercises + +**Important Notes:** + +- ⚠️ **No need to use `vocabulary_ids` anymore!** +- Just provide a simple array of words like `["mother", "father", "sister"]` +- The system will search for these words in the `Vocab` table by `base_word` field +- If a word doesn't exist in the database, the system will handle it gracefully +- For custom/inline vocabulary, you can also use detailed objects: + +```json +{ + "type": "vocabulary", + "words": [ + { + "word": "mother", + "translation": "mẹ", + "image": "https://cdn.sena.tech/vocab/mother.jpg", + "audio": "https://cdn.sena.tech/audio/mother.mp3", + "phonetic": "/ˈmʌð.ər/" + } + ] +} +``` + +**When to use simple array vs detailed objects:** + +- Use **simple array** (`["mother", "father"]`) when words already exist in `Vocab` table +- Use **detailed objects** when you need custom/temporary vocabulary not in the database + +--- + +### 2. Grammar Lesson + +**Purpose:** Teaching grammar rules with examples and practice + +**`lesson_content_type`:** `grammar` + +**`content_json` Structure:** + +```json +{ + "type": "grammar", + "grammar_points": ["Present Simple", "Present Continuous"], + "sentences": [ + "I eat an apple.", + "She eats an apple.", + "They are eating apples." + ], + "exercises": [ + { + "type": "fill_blank", + "question": "She ___ (eat) an apple every day.", + "answer": "eats", + "options": ["eat", "eats", "eating"] + } + ] +} +``` + +**Key Fields:** + +- **`grammar_points`**: Array of grammar names (e.g., `["Present Simple"]`). **System will search for matching grammar patterns in the `Grammar` table** +- **`sentences`**: Array of example sentences demonstrating the grammar +- **`exercises`**: Array of practice exercises + +**Important Notes:** + +- ⚠️ **No need to use `grammar_ids` anymore!** +- Just provide grammar names like `["Present Simple"]` or example sentences +- The system will search in the `Grammar` table by `title` or analyze sentence patterns +- You can also use just sentences without grammar_points: + +```json +{ + "type": "grammar", + "sentences": [ + "I eat an apple.", + "She eats an apple." + ], + "exercises": [...] +} +``` + +**Alternative: Inline Grammar Rules** + +For custom grammar explanations: + +```json +{ + "type": "grammar", + "rules": [ + { + "rule_name": "Present Simple", + "formula": "S + V(s/es)", + "explanation": "Used for habits and facts" + } + ], + "sentences": [...], + "exercises": [...] +} +``` + +--- + +### 3. Phonics Lesson + +**Purpose:** Teaching pronunciation with IPA and sound practice + +**`lesson_content_type`:** `phonics` + +**`content_json` Structure:** + +```json +{ + "type": "phonics", + "phonics_rules": [ + { + "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": "https://cdn.sena.tech/audio/cat.mp3", + "image": "https://cdn.sena.tech/img/cat.jpg" + }, + { + "word": "hat", + "phonetic": "/hæt/", + "audio": "https://cdn.sena.tech/audio/hat.mp3", + "image": "https://cdn.sena.tech/img/hat.jpg" + } + ] + } + ], + "exercises": [ + { + "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" + } + ] +} +``` + +**Key Fields:** + +- **`phonics_rules`**: Array of phonics rules with IPA notation +- **`ipa`**: International Phonetic Alphabet symbol (e.g., `/æ/`, `/iː/`) +- **`sound_name`**: Friendly name for the sound (e.g., "short a", "long e") +- **`words`**: Example words demonstrating the sound +- **`exercises`**: Pronunciation practice exercises + +--- + +### 4. Review Lesson + +**Purpose:** Reviewing multiple content types (vocabulary + grammar + phonics) + +**`lesson_content_type`:** `review` + +**`content_json` Structure:** + +```json +{ + "type": "review", + "sections": [ + { + "section_type": "vocabulary", + "title": "Family Vocabulary Review", + "words": ["mother", "father", "sister", "brother"], + "exercises": [ + { + "type": "match", + "question": "Match family members", + "items": [...] + } + ] + }, + { + "section_type": "grammar", + "title": "Present Simple Review", + "grammar_points": ["Present Simple"], + "sentences": ["I eat an apple", "She eats an apple"], + "exercises": [ + { + "type": "fill_blank", + "question": "She ___ an apple.", + "answer": "eats", + "options": ["eat", "eats", "eating"] + } + ] + }, + { + "section_type": "phonics", + "title": "Short A Sound Review", + "phonics_rules": [ + { + "ipa": "/æ/", + "words": ["cat", "hat", "bat"] + } + ], + "exercises": [...] + } + ], + "overall_exercises": [ + { + "type": "mixed_quiz", + "questions": [ + { + "type": "vocabulary", + "question": "What is 'mẹ' in English?", + "answer": "mother", + "options": ["mother", "father", "sister"] + }, + { + "type": "grammar", + "question": "She ___ an apple.", + "answer": "eats", + "options": ["eat", "eats", "eating"] + }, + { + "type": "phonics", + "question": "Which word has the /æ/ sound?", + "answer": "cat", + "options": ["cat", "cut", "cot"] + } + ] + } + ] +} +``` + +**Key Fields:** + +- **`sections`**: Array of review sections (vocabulary, grammar, phonics) +- **`section_type`**: Type of each section (`vocabulary`, `grammar`, `phonics`) +- **`words`**: For vocabulary sections - system will lookup in Vocab table +- **`grammar_points`** / **`sentences`**: For grammar sections - system will lookup in Grammar table +- **`overall_exercises`**: Mixed exercises combining all sections + +**Review Lesson Guidelines:** + +1. Each section should review 1-3 key concepts from previous lessons +2. Use **word arrays** for vocabulary (e.g., `["mother", "father"]`) +3. Use **sentence arrays** or grammar names for grammar (e.g., `["I eat an apple"]` or `["Present Simple"]`) +4. Include exercises for each section +5. Add `overall_exercises` for comprehensive review +6. Use `mixed_quiz` to test understanding across all content types + +--- + +## Exercise Types + +Common exercise types across all lesson types: + +### 1. Match Exercise + +**Purpose:** Match items (words to images, words to translations) + +```json +{ + "type": "match", + "question": "Match the words with pictures", + "items": [ + { "word": "mother", "image": "https://cdn.sena.tech/img/mother.jpg" }, + { "word": "father", "image": "https://cdn.sena.tech/img/father.jpg" } + ] +} +``` + +### 2. Fill in the Blank + +**Purpose:** Complete sentences with correct words + +```json +{ + "type": "fill_blank", + "question": "This is my ___", + "answer": "mother", + "options": ["mother", "father", "sister"] +} +``` + +### 3. Multiple Choice + +**Purpose:** Select correct answer from options + +```json +{ + "type": "multiple_choice", + "question": "What is 'mẹ' in English?", + "options": ["mother", "father", "sister", "brother"], + "answer": "mother" +} +``` + +### 4. Arrange Words + +**Purpose:** Arrange words to form correct sentence + +```json +{ + "type": "arrange_words", + "question": "Arrange: apple / eat / I / an", + "answer": "I eat an apple", + "words": ["apple", "eat", "I", "an"] +} +``` + +### 5. Listen and Repeat (Phonics) + +**Purpose:** Practice pronunciation + +```json +{ + "type": "listen_repeat", + "question": "Listen and repeat", + "audio": "https://cdn.sena.tech/audio/cat.mp3", + "word": "cat" +} +``` + +### 6. Identify Sound (Phonics) + +**Purpose:** Identify words with specific sound + +```json +{ + "type": "identify_sound", + "question": "Which word has the /æ/ sound?", + "options": ["cat", "cut", "cot"], + "answer": "cat" +} +``` + +### 7. True/False + +**Purpose:** Validate statements + +```json +{ + "type": "true_false", + "question": "The word 'mother' means 'mẹ'.", + "answer": true +} +``` + +### 8. Mixed Quiz (Review Lessons) + +**Purpose:** Combine multiple question types + +```json +{ + "type": "mixed_quiz", + "questions": [ + { + "type": "vocabulary", + "question": "What is 'mẹ' in English?", + "answer": "mother", + "options": ["mother", "father", "sister"] + }, + { + "type": "grammar", + "question": "She ___ an apple.", + "answer": "eats", + "options": ["eat", "eats", "eating"] + } + ] +} +``` + +--- + +## API Workflow + +### Step-by-Step Guide to Create Learning Content + +#### Step 1: Create Subject + +**Endpoint:** `POST /api/subjects` + +**Request:** + +```json +{ + "subject_code": "ENG-G1", + "subject_name": "Tiếng Anh Lớp 1", + "subject_name_en": "English Grade 1", + "description": "Basic English for Grade 1", + "is_active": true, + "is_public": true +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Subject created successfully", + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "subject_code": "ENG-G1", + ... + } +} +``` + +**Save the `id` for next step.** + +--- + +#### Step 2: Create Chapter + +**Endpoint:** `POST /api/chapters` + +**Request:** + +```json +{ + "subject_id": "550e8400-e29b-41d4-a716-446655440000", + "chapter_number": 1, + "chapter_title": "Unit 1: My Family", + "chapter_description": "Learn about family members", + "duration_minutes": 120, + "is_published": true +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Chapter created successfully", + "data": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "subject_id": "550e8400-e29b-41d4-a716-446655440000", + ... + } +} +``` + +**Save the `id` for next step.** + +--- + +#### Step 3: Create Vocabulary Lesson (using word list) + +**Endpoint:** `POST /api/learning-content/lessons` + +**Note:** ⚠️ **No need to create Vocab entries first!** Just use a simple word list. System will lookup words in Vocab table when rendering. + +**Request:** + +```json +{ + "chapter_id": "550e8400-e29b-41d4-a716-446655440001", + "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": [ + { + "type": "match", + "question": "Match family members", + "items": [ + { "word": "mother", "image": "https://cdn.sena.tech/img/mother.jpg" } + ] + } + ] + }, + "duration_minutes": 30, + "is_published": true +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Lesson created successfully", + "data": { + "id": "550e8400-e29b-41d4-a716-446655440010", + ... + } +} +``` + +--- + +#### Step 4: Create Grammar Lesson (using sentences) + +**Endpoint:** `POST /api/learning-content/lessons` + +**Note:** ⚠️ **No need to create Grammar entries first!** Just provide grammar names or example sentences. System will find matching patterns in Grammar table. + +**Request:** + +```json +{ + "chapter_id": "550e8400-e29b-41d4-a716-446655440001", + "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": [ + { + "type": "fill_blank", + "question": "She ___ an apple.", + "answer": "eats", + "options": ["eat", "eats", "eating"] + } + ] + }, + "duration_minutes": 30, + "is_published": true +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Lesson created successfully", + "data": { + "id": "550e8400-e29b-41d4-a716-446655440011", + ... + } +} +``` + +--- + +#### Step 5: Create Review Lesson + +**Endpoint:** `POST /api/learning-content/lessons` + +**Request:** + +```json +{ + "chapter_id": "550e8400-e29b-41d4-a716-446655440001", + "lesson_number": 3, + "lesson_title": "Unit Review", + "lesson_type": "json_content", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "sections": [ + { + "section_type": "vocabulary", + "title": "Family Words", + "words": ["mother", "father", "sister"] + }, + { + "section_type": "grammar", + "title": "Present Simple", + "grammar_points": ["Present Simple"], + "sentences": ["I eat an apple", "She eats an apple"] + } + ], + "overall_exercises": [ + { + "type": "mixed_quiz", + "questions": [ + { + "type": "vocabulary", + "question": "What is 'mẹ' in English?", + "answer": "mother" + } + ] + } + ] + }, + "duration_minutes": 40, + "is_published": true +} +``` + +--- + +### API Endpoints Summary + { "word": "hat", "phonetic": "/hæt/" } + ] + } + ], + "exercises": [ + { + "type": "listen_repeat", + "audio": "https://cdn.sena.tech/audio/cat.mp3", + "word": "cat" + } + ] + }, + "duration_minutes": 25, + "is_published": true +} +``` + +**Example 4: Review Lesson** + +```json +{ + "chapter_id": "550e8400-e29b-41d4-a716-446655440001", + "lesson_number": 4, + "lesson_title": "Unit 1 Review", + "lesson_type": "json_content", + "lesson_content_type": "review", + "content_json": { + "type": "review", + "sections": [ + { + "section_type": "vocabulary", + "title": "Family Words", + "vocabulary_ids": ["550e8400-e29b-41d4-a716-446655440020"] + }, + { + "section_type": "grammar", + "title": "Present Simple", + "grammar_ids": ["550e8400-e29b-41d4-a716-446655440030"] + } + ], + "overall_exercises": [ + { + "type": "mixed_quiz", + "questions": [ + { + "type": "vocabulary", + "question": "What is 'mẹ' in English?", + "answer": "mother" + } + ] + } + ] + }, + "duration_minutes": 40, + "is_published": true +} +``` + +--- + +### API Endpoints Summary + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/api/subjects` | POST | Create subject | +| `/api/subjects` | GET | Get all subjects | +| `/api/subjects/:id` | GET | Get subject by ID | +| `/api/subjects/:id` | PUT | Update subject | +| `/api/subjects/:id` | DELETE | Delete subject | +| `/api/chapters` | POST | Create chapter | +| `/api/chapters` | GET | Get all chapters | +| `/api/chapters/:id` | GET | Get chapter by ID | +| `/api/chapters/:id` | PUT | Update chapter | +| `/api/chapters/:id` | DELETE | Delete chapter | +| `/api/vocab` | POST | Create vocabulary | +| `/api/vocab` | GET | Get all vocabulary | +| `/api/grammar` | POST | Create grammar rule | +| `/api/grammar` | GET | Get all grammar rules | +| `/api/learning-content/lessons` | POST | Create lesson | +| `/api/learning-content/lessons` | GET | Get all lessons | +| `/api/learning-content/lessons/:id` | GET | Get lesson by ID | +| `/api/learning-content/lessons/:id` | PUT | Update lesson | +| `/api/learning-content/lessons/:id` | DELETE | Delete lesson | +| `/api/learning-content/lessons/chapter/:chapter_id` | GET | Get lessons by chapter | +| `/api/learning-content/guide` | GET | Get learning content guide (JSON) | + +--- + +## Code Examples + +### Example 1: Complete Workflow (JavaScript) + +```javascript +const axios = require('axios'); +const API_URL = 'http://localhost:3000/api'; +const TOKEN = 'your-auth-token'; + +// Headers with authentication +const headers = { + 'Authorization': `Bearer ${TOKEN}`, + 'Content-Type': 'application/json' +}; + +async function createLearningContent() { + try { + // Step 1: Create Subject + const subjectRes = await axios.post(`${API_URL}/subjects`, { + subject_code: 'ENG-G1', + subject_name: 'Tiếng Anh Lớp 1', + subject_name_en: 'English Grade 1', + is_public: true + }, { headers }); + + const subjectId = subjectRes.data.data.id; + console.log('Subject created:', subjectId); + + // Step 2: Create Chapter + const chapterRes = await axios.post(`${API_URL}/chapters`, { + subject_id: subjectId, + chapter_number: 1, + chapter_title: 'Unit 1: My Family', + is_published: true + }, { headers }); + + const chapterId = chapterRes.data.data.id; + console.log('Chapter created:', chapterId); + + // Step 3: Create Vocabulary Lesson (using word list - NO NEED to create Vocab first!) + const lesson1Res = await axios.post(`${API_URL}/learning-content/lessons`, { + chapter_id: chapterId, + 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: [ + { + type: 'match', + question: 'Match family members', + items: [ + { word: 'mother', image: 'https://...' } + ] + } + ] + }, + is_published: true + }, { headers }); + + console.log('Vocabulary lesson created:', lesson1Res.data.data.id); + + // Step 4: Create Grammar Lesson (using sentences - NO NEED to create Grammar first!) + const lesson2Res = await axios.post(`${API_URL}/learning-content/lessons`, { + chapter_id: chapterId, + 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: [ + { + type: 'fill_blank', + question: 'She ___ an apple.', + answer: 'eats', + options: ['eat', 'eats', 'eating'] + } + ] + }, + is_published: true + }, { headers }); + + console.log('Grammar lesson created:', lesson2Res.data.data.id); + + // Step 5: Create Review Lesson + const lesson3Res = await axios.post(`${API_URL}/learning-content/lessons`, { + chapter_id: chapterId, + lesson_number: 3, + lesson_title: 'Review Lesson', + lesson_type: 'json_content', + lesson_content_type: 'review', + content_json: { + type: 'review', + sections: [ + { + section_type: 'vocabulary', + title: 'Vocabulary Review', + words: ['mother', 'father', 'sister'] + }, + { + section_type: 'grammar', + title: 'Grammar Review', + grammar_points: ['Present Simple'], + sentences: ['I eat an apple'] + } + ], + overall_exercises: [ + { + type: 'mixed_quiz', + questions: [ + { + type: 'vocabulary', + question: 'What is "mẹ" in English?', + answer: 'mother' + } + ] + } + ] + }, + is_published: true + }, { headers }); + + console.log('Review lesson created:', lesson3Res.data.data.id); + + } catch (error) { + console.error('Error:', error.response?.data || error.message); + } +} + +createLearningContent(); +``` + +--- + +### Example 2: Query Lessons by Chapter + lesson_content_type: 'vocabulary', + content_json: { + type: 'vocabulary', + words: ['mother', 'father', 'sister'], + exercises: [ + { + type: 'match', + question: 'Match family members', + items: [ + { word: 'mother', image: 'https://...' } + ] + } + ] + }, + is_published: true + }, { headers }); + + console.log('Vocabulary lesson created:', lesson1Res.data.data.id); + + // Step 6: Create Grammar Lesson + const lesson2Res = await axios.post(`${API_URL}/learning-content/lessons`, { + chapter_id: chapterId, + 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: [ + { + type: 'fill_blank', + question: 'She ___ an apple.', + answer: 'eats', + options: ['eat', 'eats', 'eating'] + } + ] + }, + is_published: true + }, { headers }); + + console.log('Grammar lesson created:', lesson2Res.data.data.id); + + // Step 7: Create Review Lesson + const lesson3Res = await axios.post(`${API_URL}/learning-content/lessons`, { + chapter_id: chapterId, + lesson_number: 3, + lesson_title: 'Review Lesson', + lesson_type: 'json_content', + lesson_content_type: 'review', + content_json: { + type: 'review', + sections: [ + { + section_type: 'vocabulary', + title: 'Vocabulary Review', + words: ['mother', 'father', 'sister'] + }, + { + section_type: 'grammar', + title: 'Grammar Review', + grammar_points: ['Present Simple'], + sentences: ['I eat an apple'] + } + ], + overall_exercises: [ + { + type: 'mixed_quiz', + questions: [ + { + type: 'vocabulary', + question: 'What is "mẹ" in English?', + answer: 'mother' + } + ] + } + ] + }, + is_published: true + }, { headers }); + + console.log('Review lesson created:', lesson3Res.data.data.id); + + } catch (error) { + console.error('Error:', error.response?.data || error.message); + } +} + +createLearningContent(); +``` + +--- + +### Example 2: Query Lessons by Chapter + +```javascript +async function getLessonsByChapter(chapterId) { + try { + const response = await axios.get( + `${API_URL}/learning-content/lessons/chapter/${chapterId}` + ); + + console.log(`Found ${response.data.count} lessons`); + response.data.data.forEach(lesson => { + console.log(`- ${lesson.lesson_number}. ${lesson.lesson_title} (${lesson.lesson_content_type})`); + }); + + return response.data.data; + } catch (error) { + console.error('Error:', error.response?.data || error.message); + } +} +``` + +--- + +### Example 3: Update Lesson Content + +```javascript +async function updateLessonExercises(lessonId, newExercises) { + try { + // First, get the lesson + const getRes = await axios.get(`${API_URL}/learning-content/lessons/${lessonId}`); + const lesson = getRes.data.data; + + // Update content_json with new exercises + const updatedContentJson = { + ...lesson.content_json, + exercises: newExercises + }; + + // Send update request + const updateRes = await axios.put( + `${API_URL}/learning-content/lessons/${lessonId}`, + { content_json: updatedContentJson }, + { headers } + ); + + console.log('Lesson updated successfully'); + return updateRes.data.data; + } catch (error) { + console.error('Error:', error.response?.data || error.message); + } +} +``` + +--- + +## Validation Checklist + +Before creating lessons, validate: + +- [ ] **Subject exists**: Check that `subject_id` is valid +- [ ] **Chapter exists**: Check that `chapter_id` is valid +- [ ] **Lesson type matches content**: + - `lesson_type = 'json_content'` → `content_json` is present + - `lesson_type = 'url_content'` → `content_url` is present +- [ ] **Content type consistency**: `lesson_content_type` matches `content_json.type` +- [ ] **Valid UUIDs**: All `vocabulary_ids` and `grammar_ids` reference existing records +- [ ] **Exercise structure**: All exercises have required fields (`type`, `question`, `answer`) +- [ ] **URLs are accessible**: Check all `image`, `audio`, `video` URLs +- [ ] **Lesson numbers are sequential**: Lessons in a chapter are numbered 1, 2, 3, ... +- [ ] **Required fields present**: `chapter_id`, `lesson_number`, `lesson_title`, `lesson_type` + +--- + +## Common Mistakes + +### Mistake 1: Missing Parent References + +**❌ Wrong:** + +```json +{ + "lesson_title": "Family Vocabulary" +} +``` + +**✅ Correct:** + +```json +{ + "chapter_id": "550e8400-e29b-41d4-a716-446655440001", + "lesson_number": 1, + "lesson_title": "Family Vocabulary" +} +``` + +--- + +### Mistake 2: Wrong Content Type + +**❌ Wrong:** + +```json +{ + "lesson_type": "json_content", + "content_url": "https://www.youtube.com/watch?v=..." +} +``` + +**✅ Correct:** + +```json +{ + "lesson_type": "url_content", + "content_url": "https://www.youtube.com/watch?v=...", + "content_type": "video" +} +``` + +--- + +### Mistake 3: Using Old vocabulary_ids Format + +**❌ Wrong:** + +```json +{ + "content_json": { + "type": "vocabulary", + "vocabulary_ids": ["550e8400-e29b-41d4-a716-446655440020"] + } +} +``` + +**✅ Correct:** + +```json +{ + "content_json": { + "type": "vocabulary", + "words": ["mother", "father", "sister"] + } +} +``` + +**Explanation:** Use `words` array instead of `vocabulary_ids`. System will automatically lookup words in Vocab table when rendering. + +--- + +### Mistake 4: Using Old grammar_ids Format + +**❌ Wrong:** + +```json +{ + "content_json": { + "type": "grammar", + "grammar_ids": ["550e8400-e29b-41d4-a716-446655440030"] + } +} +``` + +**✅ Correct:** + +```json +{ + "content_json": { + "type": "grammar", + "grammar_points": ["Present Simple"], + "sentences": ["I eat an apple", "She eats an apple"] + } +} +``` + +**Explanation:** Use `grammar_points` or `sentences` instead of `grammar_ids`. System will find matching patterns in Grammar table. + +--- + +### Mistake 5: Missing Content Type + +**❌ Wrong:** + +```json +{ + "content_json": { + "exercises": [...] + } +} +``` + +**✅ Correct:** + +```json +{ + "content_json": { + "type": "vocabulary", + "exercises": [...] + } +} +``` + +--- + +### Mistake 5: Inconsistent Type Fields + +**❌ Wrong:** + +```json +{ + "lesson_content_type": "vocabulary", + "content_json": { + "type": "grammar", + ... + } +} +``` + +**✅ Correct:** + +```json +{ + "lesson_content_type": "vocabulary", + "content_json": { + "type": "vocabulary", + ... + } +} +``` + +--- + +## AI Integration Tips + +### Planning + +1. **Plan the hierarchy first**: Subject → Chapters → Lessons +2. **Use simple word and sentence arrays**: No need to create Vocab/Grammar entries first! +3. **Use sequential numbering**: Lesson numbers should be sequential within each chapter + +### Word Lists + +1. **For vocabulary lessons**: Use simple arrays like `["mother", "father", "sister"]` +2. **System auto-lookup**: System will automatically search for these words in `Vocab` table when rendering +3. **Graceful fallback**: If a word doesn't exist in database, system will handle it gracefully + +### Sentences + +1. **For grammar lessons**: Use sentence arrays like `["I eat an apple", "She eats an apple"]` +2. **Grammar points**: Or use grammar names like `["Present Simple"]` +3. **System matching**: System will find matching grammar patterns in `Grammar` table + +### Consistency + +1. **Keep `lesson_content_type` and `content_json.type` consistent**: They should match +2. **Use appropriate exercise types**: Match exercise types to lesson content (e.g., `listen_repeat` for phonics) + +### Exercises + +1. **Include diverse exercise types**: Use multiple exercise types for better engagement +2. **Provide answer options**: For `fill_blank` and `multiple_choice`, include 3-5 options +3. **Add multimedia**: Include `audio`, `image`, `video` URLs for richer learning experience + +### Multimedia + +1. **Provide audio for pronunciation**: Especially important for vocabulary and phonics lessons +2. **Use images for vocabulary**: Visual aids improve retention +3. **Test URLs before saving**: Ensure all media URLs are accessible + +### No IDs Needed! + +⚠️ **IMPORTANT**: You do **NOT** need to create Vocab or Grammar entries before creating lessons! + +- ✅ Just use word lists: `["mother", "father"]` +- ✅ Just use sentences: `["I eat an apple"]` +- ✅ System will lookup content when needed +- ❌ Don't use `vocabulary_ids` anymore +- ❌ Don't use `grammar_ids` anymore + +--- + +## API Guide Endpoint + +**Endpoint:** `GET /api/learning-content/guide` + +**Purpose:** Returns this guide in JSON format for AI consumption + +**Response:** + +```json +{ + "success": true, + "data": { + "guide_version": "1.0.0", + "hierarchy": {...}, + "subject_structure": {...}, + "chapter_structure": {...}, + "lesson_structure": {...}, + "lesson_content_types": {...}, + "exercise_types": {...}, + "api_workflow": {...}, + "validation_checklist": [...], + "common_mistakes": [...], + "ai_tips": {...} + } +} +``` + +**Usage:** + +```javascript +const guideRes = await axios.get(`${API_URL}/learning-content/guide`); +const guide = guideRes.data.data; + +// Access guide sections +console.log(guide.hierarchy); +console.log(guide.lesson_content_types.vocabulary_lesson); +console.log(guide.api_workflow); +``` + +--- + +## Summary + +This guide provides: + +1. **Complete hierarchy understanding**: Subject → Chapter → Lesson +2. **4 lesson content types**: Vocabulary, Grammar, Phonics, Review +3. **8 exercise types**: Match, Fill Blank, Multiple Choice, Arrange Words, Listen Repeat, Identify Sound, True/False, Mixed Quiz +4. **API workflow**: Step-by-step guide to create learning content +5. **Code examples**: JavaScript examples for common operations +6. **Validation checklist**: Ensure data quality before creation +7. **Common mistakes**: Learn from typical errors +8. **AI integration tips**: Best practices for AI-driven content creation + +**Next Steps:** + +1. Review the guide +2. Understand the hierarchy +3. Create vocabulary and grammar entries +4. Create lessons with proper content structure +5. Validate all data before saving +6. Test with small datasets first + +**For Questions:** + +- API Guide: `GET /api/learning-content/guide` +- Vocab Guide: `GET /api/vocab/guide` +- Grammar Guide: `GET /api/grammar/guide` +- Story Guide: `GET /api/stories/guide` + +--- + +**Version:** 1.0.0 +**Last Updated:** 2024 +**Maintained by:** SENA Learning Platform diff --git a/STORY_GUIDE.md b/STORY_GUIDE.md new file mode 100644 index 0000000..ea04ba2 --- /dev/null +++ b/STORY_GUIDE.md @@ -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 +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 + +{...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.** diff --git a/VOCAB_GUIDE.md b/VOCAB_GUIDE.md new file mode 100644 index 0000000..a3b1c5c --- /dev/null +++ b/VOCAB_GUIDE.md @@ -0,0 +1,1577 @@ +# 📚 VOCABULARY MANAGEMENT GUIDE + +## Tổng quan +Hệ thống Vocabulary quản lý từ vựng theo chương trình học, hỗ trợ nhiều dạng thức ngữ pháp, phát âm chuẩn xác, và tích hợp gamification. + +## 🗂️ Cấu trúc Database + +### 1. Bảng chính: `vocab` +Lưu trữ thông tin cơ bản của từ vựng. + +| Trường | Kiểu | Mô tả | +|--------|------|-------| +| vocab_id | INTEGER | ID tự động tăng | +| vocab_code | STRING(50) | Mã định danh duy nhất (ví dụ: "vocab-001-eat") | +| base_word | STRING(100) | Từ gốc | +| translation | STRING(200) | Bản dịch tiếng Việt | +| difficulty_score | INTEGER | Độ khó (1-10) | +| category | STRING(100) | Danh mục (ví dụ: "Action Verbs") | +| images | JSON | Mảng URL hình ảnh | +| tags | JSON | Mảng thẻ phân loại | +| syntax | JSON | Vai trò cú pháp (is_subject, is_verb, is_object, is_article, is_adv, verb_type, etc.) | +| semantics | JSON | Ràng buộc ngữ nghĩa (can_be_subject_type, can_take_object_type, can_modify, word_type) | +| constraints | JSON | Ràng buộc ngữ pháp (followed_by, match_subject, match_with, phonetic_rules) | +| is_active | BOOLEAN | Trạng thái hoạt động | + +### 2. Bảng phụ: `vocab_mapping` +Quản lý điểm xuất hiện của từ trong chương trình học. + +| Trường | Kiểu | Mô tả | +|--------|------|-------| +| mapping_id | INTEGER | ID tự động tăng | +| vocab_id | INTEGER | Tham chiếu đến vocab | +| book_id | STRING(100) | ID sách giáo trình | +| grade | INTEGER | Khối lớp | +| unit | INTEGER | Bài học | +| lesson | INTEGER | Tiết học | +| form_key | STRING(50) | Dạng từ sử dụng (v1, v_ing, v2, etc.) | +| context_note | TEXT | Ghi chú ngữ cảnh | + +### 3. Bảng phụ: `vocab_form` +Lưu trữ các biến thể ngữ pháp của từ. + +| Trường | Kiểu | Mô tả | +|--------|------|-------| +| form_id | INTEGER | ID tự động tăng | +| vocab_id | INTEGER | Tham chiếu đến vocab | +| form_key | STRING(50) | Khóa dạng từ (v1, v2, v3, v_ing, v_s_es, etc.) | +| text | STRING(100) | Từ thực tế (eat, eats, eating, ate) | +| phonetic | STRING(100) | Phiên âm IPA (ví dụ: /iːt/) | +| audio_url | STRING(500) | URL file phát âm | +| min_grade | INTEGER | Khối lớp tối thiểu để mở khóa | +| description | TEXT | Mô tả/ghi chú sử dụng | + +### 4. Bảng phụ: `vocab_relation` +Quản lý quan hệ ngữ nghĩa giữa các từ. + +| Trường | Kiểu | Mô tả | +|--------|------|-------| +| relation_id | INTEGER | ID tự động tăng | +| vocab_id | INTEGER | Tham chiếu đến vocab | +| relation_type | ENUM | Loại quan hệ: synonym, antonym, related | +| related_word | STRING(100) | Từ có quan hệ | +| related_vocab_id | INTEGER | Tham chiếu đến vocab liên quan (nếu có) | + +--- + +## 🔌 API Endpoints + +### 1. Tạo từ vựng mới +**POST** `/api/vocab` + +#### Request Body: +```json +{ + "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", + "context_note": "Dạy thì tiếp diễn" + } + ], + + "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" + } +} +``` + +#### Response (201): +```json +{ + "message": "Vocabulary created successfully", + "data": { + "id": "vocab-001-eat", + "vocab_id": 1, + "base_word": "eat", + "translation": "ăn", + "attributes": { ... }, + "mappings": [ ... ], + "forms": { ... }, + "relations": { ... } + } +} +``` + +--- + +### 2. Lấy danh sách từ vựng +**GET** `/api/vocab` + +#### Query Parameters: +| Tham số | Mô tả | Ví dụ | +|---------|-------|-------| +| page | Số trang (mặc định: 1) | `?page=2` | +| limit | Số lượng/trang (mặc định: 20) | `?limit=50` | +| category | Lọc theo danh mục | `?category=Action Verbs` | +| grade | Lọc theo khối lớp | `?grade=1` | +| book_id | Lọc theo sách | `?book_id=global-success-1` | +| difficulty_min | Độ khó tối thiểu | `?difficulty_min=1` | +| difficulty_max | Độ khó tối đa | `?difficulty_max=5` | +| search | Tìm kiếm từ/nghĩa | `?search=eat` | +| include_relations | Bao gồm synonyms/antonyms | `?include_relations=true` | + +#### Ví dụ: +``` +GET /api/vocab?grade=1&category=Action Verbs&limit=10 +GET /api/vocab?search=food&include_relations=true +GET /api/vocab?book_id=global-success-1&unit=2 +``` + +#### Response (200): +```json +{ + "message": "Vocabularies retrieved successfully", + "data": [ ... ], + "pagination": { + "total": 150, + "page": 1, + "limit": 20, + "totalPages": 8 + } +} +``` + +--- + +### 3. Lấy từ vựng theo ID hoặc Code +**GET** `/api/vocab/:id` + +#### Ví dụ: +``` +GET /api/vocab/1 (Lấy theo vocab_id) +GET /api/vocab/vocab-001-eat (Lấy theo vocab_code) +``` + +#### Response (200): +```json +{ + "message": "Vocabulary retrieved successfully", + "data": { + "id": "vocab-001-eat", + "vocab_id": 1, + "base_word": "eat", + "translation": "ăn", + "attributes": { + "difficulty_score": 1, + "category": "Action Verbs", + "images": [...], + "tags": [...] + }, + "mappings": [...], + "forms": {...}, + "relations": {...} + } +} +``` + +--- + +### 4. Lấy từ vựng theo chương trình học +**GET** `/api/vocab/curriculum` + +#### Query Parameters: +| Tham số | Bắt buộc | Mô tả | +|---------|----------|-------| +| book_id | Không* | ID sách giáo trình | +| grade | Không* | Khối lớp | +| unit | Không | Bài học | +| lesson | Không | Tiết học | + +*Ít nhất `book_id` hoặc `grade` phải có. + +#### Ví dụ: +``` +GET /api/vocab/curriculum?grade=1&unit=2 +GET /api/vocab/curriculum?book_id=global-success-1&grade=2&unit=5&lesson=1 +``` + +#### Response (200): +```json +{ + "message": "Vocabularies retrieved successfully", + "data": [...], + "count": 15 +} +``` + +--- + +### 5. Cập nhật từ vựng +**PUT** `/api/vocab/:id` + +#### Lưu ý: +- Có thể cập nhật một phần hoặc toàn bộ +- Khi cập nhật `mappings`, `forms`, hoặc `relations`, toàn bộ dữ liệu cũ sẽ được thay thế + +#### Request Body (Partial Update): +```json +{ + "translation": "ăn uống", + "attributes": { + "difficulty_score": 2, + "tags": ["daily-routine", "verb", "food"] + } +} +``` + +#### Request Body (Update Forms): +```json +{ + "forms": { + "v1": { + "text": "eat", + "phonetic": "/iːt/", + "audio": "https://cdn.sena.tech/audio/eat_v1_updated.mp3", + "min_grade": 1 + }, + "v2": { + "text": "ate", + "phonetic": "/eɪt/", + "audio": "https://cdn.sena.tech/audio/ate_v2_updated.mp3", + "min_grade": 3 + } + } +} +``` + +#### Response (200): +```json +{ + "message": "Vocabulary updated successfully", + "data": { ... } +} +``` + +--- + +### 6. Xóa từ vựng (Soft Delete) +**DELETE** `/api/vocab/:id` + +#### Ví dụ: +``` +DELETE /api/vocab/1 +DELETE /api/vocab/vocab-001-eat +``` + +#### Response (200): +```json +{ + "message": "Vocabulary deleted successfully" +} +``` + +--- + +### 7. Thống kê từ vựng +**GET** `/api/vocab/stats` + +#### Response (200): +```json +{ + "message": "Vocabulary statistics retrieved successfully", + "data": { + "total": 500, + "by_category": [ + { "category": "Action Verbs", "count": 120 }, + { "category": "Nouns", "count": 200 } + ], + "by_difficulty": [ + { "difficulty_score": 1, "count": 150 }, + { "difficulty_score": 2, "count": 100 } + ], + "by_grade": [ + { "grade": 1, "count": 80 }, + { "grade": 2, "count": 120 } + ] + } +} +``` + +--- + +## 📋 Quy tắc và Best Practices + +### 1. Quy tắc đặt `vocab_code` +- Format: `vocab-{sequence}-{base_word}` +- Ví dụ: `vocab-001-eat`, `vocab-002-run`, `vocab-150-beautiful` +- Phải unique trong toàn hệ thống + +### 2. Quy tắc `form_key` +Sử dụng các key chuẩn sau: + +**Động từ (Verbs):** +- `v1`: Base form (eat) +- `v_s_es`: Third person singular (eats) +- `v_ing`: Present participle (eating) +- `v2`: Past simple (ate) +- `v3`: Past participle (eaten) + +**Danh từ (Nouns):** +- `n_singular`: Số ít (cat) +- `n_plural`: Số nhiều (cats) + +**Tính từ (Adjectives):** +- `adj_base`: Dạng cơ bản (big) +- `adj_comparative`: So sánh hơn (bigger) +- `adj_superlative`: So sánh nhất (biggest) + +**Trạng từ (Adverbs):** +- `adv_base`: Dạng cơ bản (quickly) +- `adv_comparative`: So sánh hơn (more quickly) +- `adv_superlative`: So sánh nhất (most quickly) + +### 3. Quy tắc `min_grade` +- Xác định khối lớp tối thiểu để học sinh có thể truy cập dạng từ này +- Ví dụ: + - `v1` (eat): min_grade = 1 + - `v_ing` (eating): min_grade = 2 + - `v2` (ate): min_grade = 3 + +### 4. Quy tắc `difficulty_score` +| Điểm | Mô tả | +|------|-------| +| 1-2 | Cơ bản - Từ vựng thường ngày | +| 3-4 | Trung bình - Từ vựng học thuật đơn giản | +| 5-6 | Nâng cao - Từ vựng học thuật phức tạp | +| 7-8 | Khó - Từ vựng chuyên ngành | +| 9-10 | Rất khó - Từ vựng hiếm/chuyên sâu | + +### 5. Quy tắc phát âm (Phonetic) +- Sử dụng IPA (International Phonetic Alphabet) +- Đặt trong dấu `/.../ ` +- Ví dụ: `/iːt/`, `/ˈiː.tɪŋ/`, `/eɪt/` + +### 6. Quy tắc Audio URL +- Lưu trữ file âm thanh ở CDN +- Format khuyến nghị: MP3, OGG +- Naming convention: `{base_word}_{form_key}.mp3` +- Ví dụ: `eat_v1.mp3`, `eating_ing.mp3` + +### 7. Quy tắc `syntax` (Vai trò cú pháp) +Định nghĩa vai trò của từ trong câu để Grammar Engine có thể xây dựng câu đúng ngữ pháp. + +**Các trường:** +- `is_subject` (boolean): Từ có thể làm chủ ngữ không +- `is_verb` (boolean): Từ có phải động từ không +- `is_object` (boolean): Từ có thể làm tân ngữ không +- `is_be` (boolean): Từ có phải động từ "to be" không +- `is_adj` (boolean): Từ có phải tính từ không +- `verb_type` (string): Loại động từ nếu `is_verb = true` + - `"transitive"`: Động từ ngoại động (cần tân ngữ) - eat, drink, read + - `"intransitive"`: Động từ nội động (không cần tân ngữ) - run, sleep, cry + - `"linking"`: Động từ liên kết - be, become, seem + +**Ví dụ:** + +```json +// Động từ ngoại động "eat" +"syntax": { + "is_subject": false, + "is_verb": true, + "is_object": false, + "is_be": false, + "is_adj": false, + "verb_type": "transitive" +} + +// Danh từ "apple" +"syntax": { + "is_subject": true, + "is_verb": false, + "is_object": true, + "is_be": false, + "is_adj": false +} + +// Tính từ "big" +"syntax": { + "is_subject": false, + "is_verb": false, + "is_object": false, + "is_be": false, + "is_adj": true +} +``` + +### 8. Quy tắc `semantics` (Ràng buộc ngữ nghĩa) +Định nghĩa ràng buộc về mặt ngữ nghĩa để tạo câu có nghĩa logic. + +**Các trường:** +- `can_be_subject_type` (array): Loại chủ ngữ có thể đi với từ này (nếu từ là động từ) +- `can_take_object_type` (array): Loại tân ngữ mà từ này có thể nhận (nếu từ là động từ) +- `word_type` (string): Loại từ về mặt ngữ nghĩa + +**Các giá trị `subject_type` và `object_type`:** +- `"human"`: Con người (I, you, teacher, student) +- `"animal"`: Động vật (cat, dog, bird) +- `"object"`: Đồ vật vô sinh (table, chair, book) +- `"food"`: Thức ăn (apple, rice, bread) +- `"plant"`: Thực vật (tree, flower, grass) +- `"abstract"`: Khái niệm trừu tượng (love, idea, freedom) +- `"place"`: Địa điểm (school, home, park) +- `"time"`: Thời gian (morning, yesterday, hour) + +**Các giá trị `word_type`:** +- `"action"`: Hành động (eat, run, jump) +- `"state"`: Trạng thái (be, seem, appear) +- `"entity"`: Thực thể (apple, cat, table) +- `"property"`: Tính chất (big, red, happy) +- `"concept"`: Khái niệm (love, freedom, idea) + +**Ví dụ:** + +```json +// Động từ "eat" - chỉ con người/động vật ăn được thức ăn/thực vật +"semantics": { + "can_be_subject_type": ["human", "animal"], + "can_take_object_type": ["food", "plant"], + "word_type": "action" +} + +// Động từ "read" - con người đọc sách/văn bản +"semantics": { + "can_be_subject_type": ["human"], + "can_take_object_type": ["object", "abstract"], + "word_type": "action" +} + +// Danh từ "apple" +"semantics": { + "word_type": "entity" +} + +// Tính từ "happy" +"semantics": { + "word_type": "property" +} +``` + +**Tại sao cần `semantics`?** + +Grammar Engine sử dụng để tránh tạo câu vô nghĩa: +- ✅ "The **boy** eats an **apple**" (human + food) +- ❌ "The **table** eats an **apple**" (object + food - vô nghĩa!) +- ✅ "The **cat** drinks **water**" (animal + food) +- ❌ "The **cat** reads a **book**" (animal không thể read) + +--- + +## 🔧 Hệ thống Function Words & Modifiers (Grammar Engine) + +Để Grammar Engine có thể tạo câu tự nhiên và chính xác, cần bổ sung các từ chức năng và từ bổ nghĩa. + +### 9. Quy tắc `constraints` (Ràng buộc ngữ pháp) +Định nghĩa các ràng buộc khi kết hợp từ với nhau. + +**Các trường:** +- `followed_by` (string): Loại âm phải theo sau (cho articles) +- `match_subject` (object): Quy tắc khớp với chủ ngữ (cho động từ "be") +- `match_with` (string): Khớp với loại từ nào +- `phonetic_rules` (array): Quy tắc về phát âm + +### 10. Mạo từ (Articles: a, an, the) + +Mạo từ cần logic về phát âm để Engine tự động chọn đúng. + +**Cấu trúc mạo từ bất định "a":** + +```json +{ + "vocab_code": "vocab-art-01", + "base_word": "a", + "translation": "một (mạo từ bất định)", + "attributes": { + "difficulty_score": 1, + "category": "Articles", + "tags": ["article", "function-word", "grammar"] + }, + "forms": { + "base": { + "text": "a", + "phonetic": "/ə/", + "audio": "https://cdn.sena.tech/audio/a.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_article": true, + "article_type": "indefinite", + "priority": 1 + }, + "constraints": { + "followed_by": "consonant_sound" + } +} +``` + +**Cấu trúc mạo từ bất định "an":** + +```json +{ + "vocab_code": "vocab-art-02", + "base_word": "an", + "translation": "một (mạo từ bất định - trước nguyên âm)", + "attributes": { + "difficulty_score": 1, + "category": "Articles", + "tags": ["article", "function-word", "grammar"] + }, + "forms": { + "base": { + "text": "an", + "phonetic": "/ən/", + "audio": "https://cdn.sena.tech/audio/an.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_article": true, + "article_type": "indefinite", + "priority": 1 + }, + "constraints": { + "followed_by": "vowel_sound" + } +} +``` + +**Cấu trúc mạo từ xác định "the":** + +```json +{ + "vocab_code": "vocab-art-03", + "base_word": "the", + "translation": "cái, con (mạo từ xác định)", + "attributes": { + "difficulty_score": 1, + "category": "Articles", + "tags": ["article", "function-word", "grammar"] + }, + "forms": { + "base_consonant": { + "text": "the", + "phonetic": "/ðə/", + "audio": "https://cdn.sena.tech/audio/the_consonant.mp3", + "min_grade": 1, + "description": "Phát âm trước phụ âm" + }, + "base_vowel": { + "text": "the", + "phonetic": "/ðiː/", + "audio": "https://cdn.sena.tech/audio/the_vowel.mp3", + "min_grade": 1, + "description": "Phát âm trước nguyên âm" + } + }, + "syntax": { + "is_article": true, + "article_type": "definite", + "priority": 1 + }, + "constraints": { + "followed_by": "any" + } +} +``` + +**Logic Engine cho Articles:** + +Khi Engine chọn từ "apple" (`/ˈæp.əl/` - bắt đầu bằng nguyên âm): +1. Quét trường `phonetic` của "apple" +2. Phát hiện âm đầu là `/æ/` (nguyên âm) +3. Loại bỏ "a" (vì `followed_by: "consonant_sound"`) +4. Chọn "an" (vì `followed_by: "vowel_sound"`) +5. Kết quả: "**an** apple" + +### 11. Trạng từ (Adverbs) + +Trạng từ cần biết nó bổ nghĩa cho gì và vị trí trong câu. + +**Trạng từ chỉ cách thức (Manner) - "quickly":** + +```json +{ + "vocab_code": "vocab-adv-01", + "base_word": "quickly", + "translation": "nhanh chóng, một cách nhanh", + "attributes": { + "difficulty_score": 2, + "category": "Adverbs", + "tags": ["adverb", "manner", "modifier"] + }, + "forms": { + "base": { + "text": "quickly", + "phonetic": "/ˈkwɪk.li/", + "audio": "https://cdn.sena.tech/audio/quickly.mp3", + "min_grade": 2 + } + }, + "syntax": { + "is_adv": true, + "adv_type": "manner", + "position": "after_verb" + }, + "semantics": { + "can_modify": ["action_verb"], + "cannot_modify": ["stative_verb", "be_verb"], + "word_type": "property" + }, + "constraints": { + "match_with": "dynamic_verb" + } +} +``` + +**Trạng từ chỉ tần suất (Frequency) - "often":** + +```json +{ + "vocab_code": "vocab-adv-02", + "base_word": "often", + "translation": "thường xuyên", + "attributes": { + "difficulty_score": 2, + "category": "Adverbs", + "tags": ["adverb", "frequency", "modifier"] + }, + "forms": { + "base": { + "text": "often", + "phonetic": "/ˈɒf.ən/", + "audio": "https://cdn.sena.tech/audio/often.mp3", + "min_grade": 2 + } + }, + "syntax": { + "is_adv": true, + "adv_type": "frequency", + "position": "before_main_verb" + }, + "semantics": { + "can_modify": ["verb"], + "word_type": "property" + }, + "constraints": { + "position_rules": ["before_verb", "after_be", "between_auxiliary_main"] + } +} +``` + +**Trạng từ chỉ mức độ (Degree) - "very":** + +```json +{ + "vocab_code": "vocab-adv-03", + "base_word": "very", + "translation": "rất", + "attributes": { + "difficulty_score": 1, + "category": "Adverbs", + "tags": ["adverb", "degree", "modifier"] + }, + "forms": { + "base": { + "text": "very", + "phonetic": "/ˈver.i/", + "audio": "https://cdn.sena.tech/audio/very.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_adv": true, + "adv_type": "degree", + "position": "before_adj_adv" + }, + "semantics": { + "can_modify": ["adjective", "adverb"], + "cannot_modify": ["verb", "noun"], + "word_type": "property" + }, + "constraints": { + "match_with": "gradable_adjective" + } +} +``` + +### 12. Đại từ (Pronouns) + +**Đại từ nhân xưng chủ ngữ:** + +```json +{ + "vocab_code": "vocab-pron-01", + "base_word": "I", + "translation": "tôi", + "attributes": { + "difficulty_score": 1, + "category": "Pronouns", + "tags": ["pronoun", "personal", "subject"] + }, + "forms": { + "subject": { + "text": "I", + "phonetic": "/aɪ/", + "audio": "https://cdn.sena.tech/audio/I.mp3", + "min_grade": 1 + }, + "object": { + "text": "me", + "phonetic": "/miː/", + "audio": "https://cdn.sena.tech/audio/me.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_subject": true, + "is_pronoun": true, + "pronoun_type": "personal", + "person": "first", + "number": "singular" + }, + "semantics": { + "person_type": "1st", + "word_type": "entity" + }, + "constraints": { + "match_subject": { + "I": "am", + "you": "are", + "he/she/it": "is" + } + } +} +``` + +### 13. Giới từ (Prepositions) + +```json +{ + "vocab_code": "vocab-prep-01", + "base_word": "in", + "translation": "trong, ở trong", + "attributes": { + "difficulty_score": 1, + "category": "Prepositions", + "tags": ["preposition", "location", "function-word"] + }, + "forms": { + "base": { + "text": "in", + "phonetic": "/ɪn/", + "audio": "https://cdn.sena.tech/audio/in.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_preposition": true, + "prep_type": "location", + "position": "before_noun" + }, + "semantics": { + "can_take_object_type": ["place", "object", "abstract"], + "word_type": "relation" + } +} +``` + +### 14. Bảng tổng hợp Syntax Roles + +| Role Key | Loại từ | Ví dụ | Thuộc tính quan trọng | +|----------|---------|-------|----------------------| +| `is_subject` | Chủ ngữ | I, Dog, Teacher | `person_type` (1st, 2nd, 3rd), `number` | +| `is_be` | Động từ Be | am, is, are | `match_subject` (I -> am) | +| `is_verb` | Động từ | run, eat, play | `verb_type` (transitive/intransitive) | +| `is_object` | Tân ngữ | ball, apple, milk | `is_countable` (đếm được hay không) | +| `is_article` | Mạo từ | a, an, the | `constraints.followed_by` (vowel/consonant) | +| `is_adv` | Trạng từ | quickly, slowly | `adv_type`, `position`, `can_modify` | +| `is_adj` | Tính từ | beautiful, big | `position` (before_noun/after_be) | +| `is_pronoun` | Đại từ | I, you, he, she | `person`, `number`, `pronoun_type` | +| `is_preposition` | Giới từ | in, on, at | `prep_type`, `can_take_object_type` | + +### 15. Pattern Logic cho Grammar Engine + +**Ví dụ: Present Continuous với Adverb** + +```javascript +// "The cat is eating an apple quickly" +{ + "pattern_name": "Present_Continuous_Adv", + "structure": { + "pattern_logic": [ + { + "slot": "{art_s}", + "role": "is_article", + "target": "{S}", + "auto_select": true // Engine tự chọn a/an/the dựa vào phonetic + }, + { + "slot": "{S}", + "role": "is_subject", + "semantic": ["animal", "human", "object"] + }, + { + "slot": "{be}", + "role": "is_be", + "match_constraint": "subject_agreement" // Khớp với chủ ngữ + }, + { + "slot": "{V}", + "role": "is_verb", + "form": "v_ing", + "verb_type": "transitive" + }, + { + "slot": "{art_o}", + "role": "is_article", + "target": "{O}", + "auto_select": true + }, + { + "slot": "{O}", + "role": "is_object", + "semantic_match": "{V}.can_take_object_type" // Object phải khớp với verb + }, + { + "slot": "{Adv}", + "role": "is_adv", + "adv_type": "manner", + "match_with": "{V}", + "optional": true // Trạng từ là tùy chọn + } + ] + } +} +``` + +**Ví dụ: Simple Present với Frequency Adverb** + +```javascript +// "I often eat apples" +{ + "pattern_name": "Simple_Present_Freq", + "structure": { + "pattern_logic": [ + { + "slot": "{S}", + "role": "is_pronoun", + "person": "first" + }, + { + "slot": "{Adv_freq}", + "role": "is_adv", + "adv_type": "frequency", + "position": "before_main_verb" + }, + { + "slot": "{V}", + "role": "is_verb", + "form": "v1", // Base form với I/you/we/they + "verb_type": "transitive" + }, + { + "slot": "{O}", + "role": "is_object", + "form": "n_plural", // Số nhiều không cần article + "semantic_match": "{V}.can_take_object_type" + } + ] + } +} +``` + +--- + +## 🎯 Use Cases phổ biến + +### Case 1: Thêm từ vựng mới cho bài học +```bash +POST /api/vocab + +{ + "vocab_code": "vocab-150-run", + "base_word": "run", + "translation": "chạy", + "attributes": { + "difficulty_score": 1, + "category": "Action Verbs", + "images": ["https://cdn.sena.tech/img/run.png"], + "tags": ["sport", "verb", "movement"] + }, + "mappings": [ + { + "book_id": "global-success-1", + "grade": 1, + "unit": 4, + "lesson": 2, + "form_key": "v1" + } + ], + "forms": { + "v1": { + "text": "run", + "phonetic": "/rʌn/", + "audio": "https://cdn.sena.tech/audio/run_v1.mp3", + "min_grade": 1 + }, + "v_s_es": { + "text": "runs", + "phonetic": "/rʌnz/", + "audio": "https://cdn.sena.tech/audio/runs_s.mp3", + "min_grade": 2 + } + }, + "relations": { + "synonyms": ["jog", "sprint"], + "antonyms": ["walk", "stop"] + }, + "syntax": { + "is_subject": false, + "is_verb": true, + "is_object": false, + "is_be": false, + "is_adj": false, + "verb_type": "intransitive" + }, + "semantics": { + "can_be_subject_type": ["human", "animal"], + "can_take_object_type": [], + "word_type": "action" + } +} +``` + +### Case 2: Thêm danh từ với semantic constraints +```bash +POST /api/vocab + +{ + "vocab_code": "vocab-200-apple", + "base_word": "apple", + "translation": "quả táo", + "attributes": { + "difficulty_score": 1, + "category": "Nouns", + "images": ["https://cdn.sena.tech/img/apple.png"], + "tags": ["food", "fruit", "noun"] + }, + "mappings": [ + { + "book_id": "global-success-1", + "grade": 1, + "unit": 3, + "lesson": 1, + "form_key": "n_singular" + } + ], + "forms": { + "n_singular": { + "text": "apple", + "phonetic": "/ˈæp.əl/", + "audio": "https://cdn.sena.tech/audio/apple_singular.mp3", + "min_grade": 1 + }, + "n_plural": { + "text": "apples", + "phonetic": "/ˈæp.əlz/", + "audio": "https://cdn.sena.tech/audio/apples_plural.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_subject": true, + "is_verb": false, + "is_object": true, + "is_be": false, + "is_adj": false + }, + "semantics": { + "word_type": "entity" + } +} +``` + +### Case 3: Lấy tất cả từ vựng cho Grade 1, Unit 2 +```bash +GET /api/vocab/curriculum?grade=1&unit=2 +``` + +### Case 4: Tìm kiếm từ vựng có liên quan đến "food" +```bash +GET /api/vocab?search=food&include_relations=true +``` + +### Case 5: Cập nhật bản dịch và thêm tag mới +```bash +PUT /api/vocab/vocab-001-eat + +{ + "translation": "ăn, dùng bữa", + "attributes": { + "tags": ["daily-routine", "verb", "food", "health"] + } +} +``` + +### Case 6: Cập nhật syntax và semantics +```bash +PUT /api/vocab/vocab-001-eat + +{ + "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" + } +} +``` + +### Case 8: Thêm mạo từ "a" với phonetic constraints +```bash +POST /api/vocab + +{ + "vocab_code": "vocab-art-01", + "base_word": "a", + "translation": "một (mạo từ bất định)", + "attributes": { + "difficulty_score": 1, + "category": "Articles", + "tags": ["article", "function-word", "grammar"] + }, + "forms": { + "base": { + "text": "a", + "phonetic": "/ə/", + "audio": "https://cdn.sena.tech/audio/a.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_article": true, + "article_type": "indefinite", + "priority": 1 + }, + "constraints": { + "followed_by": "consonant_sound" + } +} +``` + +### Case 9: Thêm trạng từ với modifier constraints +```bash +POST /api/vocab + +{ + "vocab_code": "vocab-adv-01", + "base_word": "quickly", + "translation": "nhanh chóng", + "attributes": { + "difficulty_score": 2, + "category": "Adverbs", + "tags": ["adverb", "manner", "modifier"] + }, + "forms": { + "base": { + "text": "quickly", + "phonetic": "/ˈkwɪk.li/", + "audio": "https://cdn.sena.tech/audio/quickly.mp3", + "min_grade": 2 + } + }, + "syntax": { + "is_adv": true, + "adv_type": "manner", + "position": "after_verb" + }, + "semantics": { + "can_modify": ["action_verb"], + "cannot_modify": ["stative_verb", "be_verb"], + "word_type": "property" + }, + "constraints": { + "match_with": "dynamic_verb" + } +} +``` + +### Case 10: Thêm đại từ với subject agreement +```bash +POST /api/vocab + +{ + "vocab_code": "vocab-pron-01", + "base_word": "I", + "translation": "tôi", + "attributes": { + "difficulty_score": 1, + "category": "Pronouns", + "tags": ["pronoun", "personal", "subject"] + }, + "forms": { + "subject": { + "text": "I", + "phonetic": "/aɪ/", + "audio": "https://cdn.sena.tech/audio/I.mp3", + "min_grade": 1 + }, + "object": { + "text": "me", + "phonetic": "/miː/", + "audio": "https://cdn.sena.tech/audio/me.mp3", + "min_grade": 1 + } + }, + "syntax": { + "is_subject": true, + "is_pronoun": true, + "pronoun_type": "personal", + "person": "first", + "number": "singular" + }, + "semantics": { + "person_type": "1st", + "word_type": "entity" + }, + "constraints": { + "match_subject": { + "I": "am", + "you": "are", + "he/she/it": "is" + } + } +} +``` + +### Case 11: Thêm động từ "be" với subject matching +```bash +POST /api/vocab + +{ + "vocab_code": "vocab-be-01", + "base_word": "be", + "translation": "là, thì", + "attributes": { + "difficulty_score": 1, + "category": "Verbs", + "tags": ["verb", "be-verb", "linking-verb"] + }, + "forms": { + "base": { + "text": "be", + "phonetic": "/biː/", + "audio": "https://cdn.sena.tech/audio/be.mp3", + "min_grade": 1 + }, + "am": { + "text": "am", + "phonetic": "/æm/", + "audio": "https://cdn.sena.tech/audio/am.mp3", + "min_grade": 1, + "description": "Dùng với 'I'" + }, + "is": { + "text": "is", + "phonetic": "/ɪz/", + "audio": "https://cdn.sena.tech/audio/is.mp3", + "min_grade": 1, + "description": "Dùng với he/she/it" + }, + "are": { + "text": "are", + "phonetic": "/ɑːr/", + "audio": "https://cdn.sena.tech/audio/are.mp3", + "min_grade": 1, + "description": "Dùng với you/we/they" + } + }, + "syntax": { + "is_verb": true, + "is_be": true, + "verb_type": "linking" + }, + "constraints": { + "match_subject": { + "I": "am", + "he": "is", + "she": "is", + "it": "is", + "you": "are", + "we": "are", + "they": "are" + } + } +} +``` + +### Case 12: Lấy tất cả articles cho Grammar Engine +```bash +GET /api/vocab?category=Articles +``` + +### Case 13: Lấy tất cả adverbs có thể bổ nghĩa cho action verbs +```bash +# Filter trong application logic dựa trên semantics.can_modify +GET /api/vocab?category=Adverbs&include_relations=true +``` + +### Case 14: Cập nhật constraints cho từ hiện có +```bash +PUT /api/vocab/vocab-001-eat + +{ + "constraints": { + "requires_object": true, + "cannot_be_passive": false, + "semantic_object_types": ["food", "plant"] + } +} +``` + +--- + +## ⚠️ Lưu ý quan trọng + +### 1. Transaction Safety +- Tất cả các thao tác tạo/cập nhật đều sử dụng transaction +- Nếu bất kỳ phần nào thất bại, toàn bộ thao tác sẽ rollback + +### 2. Cascade Delete +- Khi xóa vocab, các bản ghi liên quan trong `vocab_mapping`, `vocab_form`, và `vocab_relation` cũng tự động xóa +- Sử dụng soft delete để có thể khôi phục + +### 3. Indexing +- Các trường `vocab_code`, `base_word`, `category` đã được index +- Tìm kiếm theo các trường này sẽ nhanh hơn + +### 4. Validation +- `vocab_code` phải unique +- `base_word` và `translation` là bắt buộc +- `form_key` trong `vocab_form` phải unique cho mỗi vocab_id + +--- + +## 🔍 Troubleshooting + +### Lỗi: "vocab_code already exists" +- Kiểm tra xem code đã tồn tại chưa +- Sử dụng code khác hoặc xem lại vocab hiện có + +### Lỗi: "At least book_id or grade is required" +- Khi dùng endpoint `/curriculum`, phải cung cấp ít nhất `book_id` hoặc `grade` + +### Lỗi: "Vocabulary not found" +- Kiểm tra ID hoặc code có đúng không +- Kiểm tra từ vựng có bị soft delete (`is_active = false`) không + +--- + +## 📊 Database Migration + +Để tạo các bảng trong database, chạy lệnh: + +```bash +npm run sync-database +``` + +Hoặc sử dụng script đồng bộ: + +```javascript +const { syncDatabase } = require('./models'); + +// Development only - force sync +await syncDatabase({ force: true }); + +// Production - alter existing tables +await syncDatabase({ alter: true }); +``` + +--- + +## 🚀 Testing với Swagger + +Truy cập: `http://localhost:3000/api-docs` + +Tìm section **Vocabulary** để test các endpoint. + +--- + +## 🎮 Grammar Engine Integration Summary + +### Quy trình tạo câu tự động + +**Bước 1: Chọn Pattern** +```javascript +// Engine chọn pattern "Present_Continuous_Adv" +pattern = "The {art_s} {S} {be} {V_ing} {art_o} {O} {Adv}" +``` + +**Bước 2: Chọn từ theo Roles và Constraints** +```javascript +// 1. Chọn Subject (animal) +S = "cat" (syntax.is_subject=true, semantics.word_type="entity") + +// 2. Auto-select Article cho Subject +// Engine đọc phonetic của "cat" = /kæt/ (bắt đầu phụ âm) +art_s = "the" (constraints.followed_by="any") + +// 3. Match Be verb với Subject +// "cat" là số ít, ngôi 3 -> is +be = "is" (constraints.match_subject["it"]="is") + +// 4. Chọn Verb (transitive) +V = "eat" (syntax.is_verb=true, verb_type="transitive", form="v_ing") + +// 5. Chọn Object phù hợp với Verb +// eat.semantics.can_take_object_type = ["food", "plant"] +O = "apple" (semantics.word_type="entity", category="food") + +// 6. Auto-select Article cho Object +// "apple" phonetic = /ˈæp.əl/ (bắt đầu nguyên âm) +art_o = "an" (constraints.followed_by="vowel_sound") + +// 7. Chọn Adverb phù hợp với Verb +// quickly.semantics.can_modify = ["action_verb"] +// eat là action_verb -> match! +Adv = "quickly" (syntax.is_adv=true, adv_type="manner") +``` + +**Kết quả:** +``` +"The cat is eating an apple quickly" +``` + +### Kiểm tra tính hợp lệ + +**Semantic Check:** +- ✅ cat (animal) CAN eat (can_be_subject_type includes "animal") +- ✅ eat CAN take apple (can_take_object_type includes "food") +- ✅ quickly CAN modify eat (can_modify includes "action_verb") + +**Syntax Check:** +- ✅ "the" before consonant sound /k/ +- ✅ "an" before vowel sound /æ/ +- ✅ "is" matches with singular third person (cat) +- ✅ "quickly" in correct position (after_verb) + +### Database Query Examples cho Grammar Engine + +**Query 1: Lấy tất cả từ có thể làm chủ ngữ động vật** +```sql +SELECT * FROM vocab +WHERE JSON_EXTRACT(syntax, '$.is_subject') = true +AND JSON_CONTAINS(semantics, '"animal"', '$.word_type'); +``` + +**Query 2: Lấy động từ transitive có thể nhận object loại "food"** +```sql +SELECT * FROM vocab +WHERE JSON_EXTRACT(syntax, '$.is_verb') = true +AND JSON_EXTRACT(syntax, '$.verb_type') = 'transitive' +AND JSON_CONTAINS(semantics, '"food"', '$.can_take_object_type'); +``` + +**Query 3: Lấy mạo từ cho từ bắt đầu bằng nguyên âm** +```sql +SELECT * FROM vocab +WHERE JSON_EXTRACT(syntax, '$.is_article') = true +AND JSON_EXTRACT(constraints, '$.followed_by') = 'vowel_sound'; +``` + +**Query 4: Lấy trạng từ có thể bổ nghĩa cho action verb** +```sql +SELECT * FROM vocab +WHERE JSON_EXTRACT(syntax, '$.is_adv') = true +AND JSON_CONTAINS(semantics, '"action_verb"', '$.can_modify'); +``` + +### Ví dụ hoàn chỉnh: Tạo 10 câu tự động + +```javascript +// Pseudo code cho Grammar Engine +async function generateSentences(pattern, count = 10) { + const sentences = []; + + for (let i = 0; i < count; i++) { + // 1. Lấy Subject candidates + const subjects = await Vocab.findAll({ + where: sequelize.literal("JSON_EXTRACT(syntax, '$.is_subject') = true") + }); + const S = randomPick(subjects); + + // 2. Auto-select Article cho Subject + const firstSound = getFirstPhoneme(S.forms.base.phonetic); + const art_s = await selectArticle(firstSound); + + // 3. Match Be verb + const be = await matchBeVerb(S); + + // 4. Lấy Verb candidates (transitive only) + const verbs = await Vocab.findAll({ + where: sequelize.literal(` + JSON_EXTRACT(syntax, '$.is_verb') = true + AND JSON_EXTRACT(syntax, '$.verb_type') = 'transitive' + `) + }); + const V = randomPick(verbs); + + // 5. Lấy Object phù hợp với Verb + const objectTypes = V.semantics.can_take_object_type; + const objects = await Vocab.findAll({ + where: sequelize.literal(` + JSON_EXTRACT(syntax, '$.is_object') = true + AND JSON_EXTRACT(semantics, '$.word_type') IN (${objectTypes}) + `) + }); + const O = randomPick(objects); + + // 6. Auto-select Article cho Object + const objFirstSound = getFirstPhoneme(O.forms.base.phonetic); + const art_o = await selectArticle(objFirstSound); + + // 7. Lấy Adverb phù hợp + const adverbs = await Vocab.findAll({ + where: sequelize.literal(` + JSON_EXTRACT(syntax, '$.is_adv') = true + AND JSON_CONTAINS(semantics, '"action_verb"', '$.can_modify') + `) + }); + const Adv = randomPick(adverbs); + + // 8. Construct sentence + sentences.push( + `${art_s.base_word} ${S.base_word} ${be.form} ${V.forms.v_ing.text} ${art_o.base_word} ${O.base_word} ${Adv.base_word}` + ); + } + + return sentences; +} + +// Output: +// [ +// "The cat is eating an apple quickly", +// "A dog is drinking the water slowly", +// "The boy is reading a book carefully", +// ... +// ] +``` + +--- + +## 📞 Hỗ trợ + +Nếu cần hỗ trợ, liên hệ team development hoặc tham khảo tài liệu API đầy đủ tại Swagger UI. + +--- + +## 📚 Tài liệu tham khảo + +### Phonetic Resources +- **IPA Chart**: https://www.internationalphoneticalphabet.org/ +- **Cambridge Dictionary**: Nguồn phát âm chuẩn +- **Forvo**: Phát âm bởi người bản xứ + +### Grammar Resources +- **English Grammar Rules**: https://www.englishgrammar.org/ +- **Parts of Speech**: https://www.grammarly.com/blog/parts-of-speech/ +- **Verb Types**: Transitive, Intransitive, Linking verbs + +### Best Practices +1. **Consistency**: Luôn sử dụng cùng một convention cho vocab_code +2. **Completeness**: Điền đầy đủ syntax, semantics, constraints cho mỗi từ +3. **Accuracy**: Kiểm tra kỹ phonetic transcription +4. **Testing**: Test Grammar Engine với nhiều combinations +5. **Documentation**: Ghi chú rõ ràng cho từng trường đặc biệt + +--- + +**Version**: 2.0.0 (Grammar Engine Ready) +**Last Updated**: January 26, 2026 +**Maintainer**: SENA Development Team + diff --git a/app.js b/app.js index 87056ed..ba154e7 100644 --- a/app.js +++ b/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 diff --git a/controllers/grammarController.js b/controllers/grammarController.js new file mode 100644 index 0000000..9f0cea5 --- /dev/null +++ b/controllers/grammarController.js @@ -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 + }); + } +}; diff --git a/controllers/learningContentController.js b/controllers/learningContentController.js new file mode 100644 index 0000000..ff20542 --- /dev/null +++ b/controllers/learningContentController.js @@ -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: "", + 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: "", + 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: "", + 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: "", + 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: "", 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 + }); + } +}; diff --git a/controllers/storyController.js b/controllers/storyController.js new file mode 100644 index 0000000..c795285 --- /dev/null +++ b/controllers/storyController.js @@ -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 + }); + } +}; diff --git a/controllers/vocabController.js b/controllers/vocabController.js new file mode 100644 index 0000000..bf44a7f --- /dev/null +++ b/controllers/vocabController.js @@ -0,0 +1,1146 @@ +const { Vocab, VocabMapping, VocabForm, VocabRelation } = require('../models'); +const { Op } = require('sequelize'); + +/** + * @swagger + * components: + * schemas: + * Vocab: + * type: object + * required: + * - vocab_code + * - base_word + * - translation + * properties: + * vocab_id: + * type: integer + * description: Auto-generated vocab ID + * vocab_code: + * type: string + * description: Unique vocabulary code (e.g., vocab-001-eat) + * base_word: + * type: string + * description: Base form of the word + * translation: + * type: string + * description: Vietnamese translation + * difficulty_score: + * type: integer + * description: Difficulty level (1-10) + * category: + * type: string + * description: Category (e.g., Action Verbs, Nouns) + * images: + * type: array + * items: + * type: string + * description: Array of image URLs + * tags: + * type: array + * items: + * type: string + * description: Array of tags + * VocabComplete: + * allOf: + * - $ref: '#/components/schemas/Vocab' + * - type: object + * properties: + * mappings: + * type: array + * items: + * type: object + * properties: + * book_id: + * type: string + * grade: + * type: integer + * unit: + * type: integer + * lesson: + * type: integer + * form_key: + * type: string + * forms: + * type: object + * additionalProperties: + * type: object + * properties: + * text: + * type: string + * phonetic: + * type: string + * audio: + * type: string + * min_grade: + * type: integer + * relations: + * type: object + * properties: + * synonyms: + * type: array + * items: + * type: string + * antonyms: + * type: array + * items: + * type: string + */ + +/** + * Create a new vocabulary entry with all related data + */ +exports.createVocab = async (req, res) => { + const transaction = await Vocab.sequelize.transaction(); + + try { + const { + vocab_code, + base_word, + translation, + attributes = {}, + mappings = [], + forms = {}, + relations = {}, + syntax = {}, + semantics = {}, + constraints = {} + } = req.body; + + // 1. Create main vocab entry + const vocab = await Vocab.create({ + vocab_code, + base_word, + translation, + difficulty_score: attributes.difficulty_score || 1, + category: attributes.category, + images: attributes.images || [], + tags: attributes.tags || [], + syntax: syntax, + semantics: semantics, + constraints: constraints + }, { transaction }); + + // 2. Create curriculum mappings + if (mappings.length > 0) { + const mappingData = mappings.map(m => ({ + vocab_id: vocab.vocab_id, + book_id: m.book_id, + grade: m.grade, + unit: m.unit, + lesson: m.lesson, + form_key: m.form_key, + context_note: m.context_note + })); + await VocabMapping.bulkCreate(mappingData, { transaction }); + } + + // 3. Create word forms + if (Object.keys(forms).length > 0) { + const formData = Object.entries(forms).map(([form_key, formInfo]) => ({ + vocab_id: vocab.vocab_id, + form_key, + text: formInfo.text, + phonetic: formInfo.phonetic, + audio_url: formInfo.audio, + min_grade: formInfo.min_grade || 1, + description: formInfo.description + })); + await VocabForm.bulkCreate(formData, { transaction }); + } + + // 4. Create relations (synonyms and antonyms) + const relationData = []; + if (relations.synonyms && relations.synonyms.length > 0) { + relations.synonyms.forEach(word => { + relationData.push({ + vocab_id: vocab.vocab_id, + relation_type: 'synonym', + related_word: word + }); + }); + } + if (relations.antonyms && relations.antonyms.length > 0) { + relations.antonyms.forEach(word => { + relationData.push({ + vocab_id: vocab.vocab_id, + relation_type: 'antonym', + related_word: word + }); + }); + } + if (relationData.length > 0) { + await VocabRelation.bulkCreate(relationData, { transaction }); + } + + await transaction.commit(); + + // Fetch complete vocab data + const completeVocab = await getCompleteVocab(vocab.vocab_id); + + res.status(201).json({ + message: 'Vocabulary created successfully', + data: completeVocab + }); + + } catch (error) { + await transaction.rollback(); + console.error('Error creating vocab:', error); + res.status(500).json({ + message: 'Error creating vocabulary', + error: error.message + }); + } +}; + +/** + * Get all vocabulary entries with pagination and filters + */ +exports.getAllVocabs = async (req, res) => { + try { + const { + page = 1, + limit = 20, + category, + grade, + book_id, + difficulty_min, + difficulty_max, + search, + include_relations = 'false' + } = req.query; + + const offset = (page - 1) * 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] = [ + { base_word: { [Op.like]: `%${search}%` } }, + { translation: { [Op.like]: `%${search}%` } }, + { vocab_code: { [Op.like]: `%${search}%` } } + ]; + } + + // Build include array + const include = [ + { + model: VocabMapping, + as: 'mappings', + required: false + }, + { + model: VocabForm, + as: 'forms', + required: false + } + ]; + + if (include_relations === 'true') { + include.push({ + model: VocabRelation, + as: 'relations', + required: false + }); + } + + // Apply grade or book_id filter through mappings + if (grade || book_id) { + const mappingWhere = {}; + if (grade) mappingWhere.grade = parseInt(grade); + if (book_id) mappingWhere.book_id = book_id; + + include[0].where = mappingWhere; + include[0].required = true; + } + + const { count, rows } = await Vocab.findAndCountAll({ + where, + include, + limit: parseInt(limit), + offset: parseInt(offset), + order: [['vocab_code', 'ASC']], + distinct: true + }); + + // Format response + const formattedVocabs = rows.map(vocab => formatVocabResponse(vocab)); + + res.json({ + message: 'Vocabularies retrieved successfully', + data: formattedVocabs, + pagination: { + total: count, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(count / limit) + } + }); + + } catch (error) { + console.error('Error getting vocabs:', error); + res.status(500).json({ + message: 'Error retrieving vocabularies', + error: error.message + }); + } +}; + +/** + * Get single vocabulary by ID or code + */ +exports.getVocabById = async (req, res) => { + try { + const { id } = req.params; + + // Check if id is numeric (vocab_id) or string (vocab_code) + const where = isNaN(id) + ? { vocab_code: id, is_active: true } + : { vocab_id: parseInt(id), is_active: true }; + + const vocab = await Vocab.findOne({ + where, + include: [ + { model: VocabMapping, as: 'mappings' }, + { model: VocabForm, as: 'forms' }, + { model: VocabRelation, as: 'relations' } + ] + }); + + if (!vocab) { + return res.status(404).json({ + message: 'Vocabulary not found' + }); + } + + const formattedVocab = formatVocabResponse(vocab); + + res.json({ + message: 'Vocabulary retrieved successfully', + data: formattedVocab + }); + + } catch (error) { + console.error('Error getting vocab:', error); + res.status(500).json({ + message: 'Error retrieving vocabulary', + error: error.message + }); + } +}; + +/** + * Update vocabulary entry + */ +exports.updateVocab = async (req, res) => { + const transaction = await Vocab.sequelize.transaction(); + + try { + const { id } = req.params; + const { + base_word, + translation, + attributes, + mappings, + forms, + relations, + syntax, + semantics, + constraints + } = req.body; + + // Find vocab + const where = isNaN(id) ? { vocab_code: id } : { vocab_id: parseInt(id) }; + const vocab = await Vocab.findOne({ where }); + + if (!vocab) { + await transaction.rollback(); + return res.status(404).json({ + message: 'Vocabulary not found' + }); + } + + // 1. Update main vocab entry + const updateData = {}; + if (base_word) updateData.base_word = base_word; + if (translation) updateData.translation = translation; + if (attributes) { + if (attributes.difficulty_score !== undefined) updateData.difficulty_score = attributes.difficulty_score; + if (attributes.category !== undefined) updateData.category = attributes.category; + if (attributes.images !== undefined) updateData.images = attributes.images; + if (attributes.tags !== undefined) updateData.tags = attributes.tags; + } + if (syntax !== undefined) updateData.syntax = syntax; + if (semantics !== undefined) updateData.semantics = semantics; + if (constraints !== undefined) updateData.constraints = constraints; + + if (Object.keys(updateData).length > 0) { + await vocab.update(updateData, { transaction }); + } + + // 2. Update mappings (replace all) + if (mappings !== undefined) { + await VocabMapping.destroy({ where: { vocab_id: vocab.vocab_id }, transaction }); + if (mappings.length > 0) { + const mappingData = mappings.map(m => ({ + vocab_id: vocab.vocab_id, + book_id: m.book_id, + grade: m.grade, + unit: m.unit, + lesson: m.lesson, + form_key: m.form_key, + context_note: m.context_note + })); + await VocabMapping.bulkCreate(mappingData, { transaction }); + } + } + + // 3. Update forms (replace all) + if (forms !== undefined) { + await VocabForm.destroy({ where: { vocab_id: vocab.vocab_id }, transaction }); + if (Object.keys(forms).length > 0) { + const formData = Object.entries(forms).map(([form_key, formInfo]) => ({ + vocab_id: vocab.vocab_id, + form_key, + text: formInfo.text, + phonetic: formInfo.phonetic, + audio_url: formInfo.audio, + min_grade: formInfo.min_grade || 1, + description: formInfo.description + })); + await VocabForm.bulkCreate(formData, { transaction }); + } + } + + // 4. Update relations (replace all) + if (relations !== undefined) { + await VocabRelation.destroy({ where: { vocab_id: vocab.vocab_id }, transaction }); + const relationData = []; + if (relations.synonyms && relations.synonyms.length > 0) { + relations.synonyms.forEach(word => { + relationData.push({ + vocab_id: vocab.vocab_id, + relation_type: 'synonym', + related_word: word + }); + }); + } + if (relations.antonyms && relations.antonyms.length > 0) { + relations.antonyms.forEach(word => { + relationData.push({ + vocab_id: vocab.vocab_id, + relation_type: 'antonym', + related_word: word + }); + }); + } + if (relationData.length > 0) { + await VocabRelation.bulkCreate(relationData, { transaction }); + } + } + + await transaction.commit(); + + // Fetch updated vocab + const updatedVocab = await getCompleteVocab(vocab.vocab_id); + + res.json({ + message: 'Vocabulary updated successfully', + data: updatedVocab + }); + + } catch (error) { + await transaction.rollback(); + console.error('Error updating vocab:', error); + res.status(500).json({ + message: 'Error updating vocabulary', + error: error.message + }); + } +}; + +/** + * Delete vocabulary (soft delete) + */ +exports.deleteVocab = async (req, res) => { + try { + const { id } = req.params; + + const where = isNaN(id) ? { vocab_code: id } : { vocab_id: parseInt(id) }; + const vocab = await Vocab.findOne({ where }); + + if (!vocab) { + return res.status(404).json({ + message: 'Vocabulary not found' + }); + } + + await vocab.update({ is_active: false }); + + res.json({ + message: 'Vocabulary deleted successfully' + }); + + } catch (error) { + console.error('Error deleting vocab:', error); + res.status(500).json({ + message: 'Error deleting vocabulary', + error: error.message + }); + } +}; + +/** + * Get vocabularies by curriculum (book, grade, unit, lesson) + */ +exports.getVocabsByCurriculum = async (req, res) => { + try { + const { book_id, grade, unit, lesson } = req.query; + + if (!book_id && !grade) { + return res.status(400).json({ + message: 'At least 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 vocabs = await Vocab.findAll({ + where: { is_active: true }, + include: [ + { + model: VocabMapping, + as: 'mappings', + where: mappingWhere, + required: true + }, + { + model: VocabForm, + as: 'forms' + }, + { + model: VocabRelation, + as: 'relations' + } + ], + order: [['vocab_code', 'ASC']] + }); + + const formattedVocabs = vocabs.map(vocab => formatVocabResponse(vocab)); + + res.json({ + message: 'Vocabularies retrieved successfully', + data: formattedVocabs, + count: formattedVocabs.length + }); + + } catch (error) { + console.error('Error getting vocabs by curriculum:', error); + res.status(500).json({ + message: 'Error retrieving vocabularies', + error: error.message + }); + } +}; + +/** + * Get vocabulary statistics + */ +exports.getVocabStats = async (req, res) => { + try { + const { sequelize } = Vocab; + + // Total vocabs + const totalVocabs = await Vocab.count({ where: { is_active: true } }); + + // By category + const byCategory = await Vocab.findAll({ + where: { is_active: true }, + attributes: [ + 'category', + [sequelize.fn('COUNT', sequelize.col('vocab_id')), 'count'] + ], + group: ['category'], + raw: true + }); + + // By difficulty + const byDifficulty = await Vocab.findAll({ + where: { is_active: true }, + attributes: [ + 'difficulty_score', + [sequelize.fn('COUNT', sequelize.col('vocab_id')), 'count'] + ], + group: ['difficulty_score'], + order: [['difficulty_score', 'ASC']], + raw: true + }); + + // By grade (from mappings) + const byGrade = await VocabMapping.findAll({ + attributes: [ + 'grade', + [sequelize.fn('COUNT', sequelize.fn('DISTINCT', sequelize.col('vocab_id'))), 'count'] + ], + group: ['grade'], + order: [['grade', 'ASC']], + raw: true + }); + + res.json({ + message: 'Vocabulary statistics retrieved successfully', + data: { + total: totalVocabs, + by_category: byCategory, + by_difficulty: byDifficulty, + by_grade: byGrade + } + }); + + } catch (error) { + console.error('Error getting vocab stats:', error); + res.status(500).json({ + message: 'Error retrieving statistics', + error: error.message + }); + } +}; + +/** + * Get comprehensive guide for AI to create vocabulary entries + */ +exports.getVocabGuide = async (req, res) => { + try { + const guide = { + guide_version: "2.0.0", + last_updated: "2026-01-26", + description: "Comprehensive guide for AI to understand and create vocabulary entries for Grammar Engine", + + data_structure: { + required_fields: { + vocab_code: { + type: "string", + format: "vocab-{sequence}-{base_word}", + example: "vocab-001-eat", + rule: "Must be unique, use 3-digit sequence number" + }, + base_word: { + type: "string", + example: "eat", + rule: "Base form of the word in English" + }, + translation: { + type: "string", + example: "ăn", + rule: "Vietnamese translation" + } + }, + + optional_fields: { + attributes: { + difficulty_score: { + type: "integer", + range: "1-10", + default: 1, + guide: "1-2: Basic, 3-4: Intermediate, 5-6: Advanced, 7-8: Difficult, 9-10: Expert" + }, + category: { + type: "string", + options: ["Action Verbs", "Nouns", "Adjectives", "Adverbs", "Articles", "Pronouns", "Prepositions", "Conjunctions"], + example: "Action Verbs" + }, + images: { + type: "array", + item_type: "string (URL)", + example: ["https://cdn.sena.tech/img/eat-main.png"] + }, + tags: { + type: "array", + item_type: "string", + example: ["daily-routine", "verb", "food"] + } + }, + + mappings: { + type: "array of objects", + description: "Curriculum mapping - where this word appears in textbooks", + fields: { + book_id: { type: "string", example: "global-success-1" }, + grade: { type: "integer", example: 1 }, + unit: { type: "integer", example: 2 }, + lesson: { type: "integer", example: 3 }, + form_key: { type: "string", example: "v1", description: "Which form to use at this point" }, + context_note: { type: "string", optional: true } + } + }, + + forms: { + type: "object", + description: "Different grammatical forms of the word", + structure: { + "{form_key}": { + text: { type: "string", example: "eat" }, + phonetic: { type: "string", format: "IPA", example: "/iːt/" }, + audio: { type: "string", format: "URL", example: "https://cdn.sena.tech/audio/eat_v1.mp3" }, + min_grade: { type: "integer", example: 1, description: "Minimum grade to unlock" }, + description: { type: "string", optional: true } + } + } + }, + + relations: { + type: "object", + fields: { + synonyms: { type: "array", example: ["consume", "dine"] }, + antonyms: { type: "array", example: ["fast", "starve"] }, + related: { type: "array", example: ["food", "meal"], optional: true } + } + }, + + syntax: { + type: "object", + description: "Syntax roles for Grammar Engine", + critical: true, + fields: { + is_subject: { type: "boolean", description: "Can be used as subject" }, + is_verb: { type: "boolean", description: "Is a verb" }, + is_object: { type: "boolean", description: "Can be used as object" }, + is_be: { type: "boolean", description: "Is 'be' verb" }, + is_adj: { type: "boolean", description: "Is adjective" }, + is_adv: { type: "boolean", description: "Is adverb" }, + is_article: { type: "boolean", description: "Is article (a/an/the)" }, + is_pronoun: { type: "boolean", description: "Is pronoun" }, + is_preposition: { type: "boolean", description: "Is preposition" }, + verb_type: { type: "string", options: ["transitive", "intransitive", "linking"], when: "is_verb=true" }, + article_type: { type: "string", options: ["definite", "indefinite"], when: "is_article=true" }, + adv_type: { type: "string", options: ["manner", "frequency", "degree", "time", "place"], when: "is_adv=true" }, + position: { type: "string", description: "Word position in sentence" }, + priority: { type: "integer", description: "Selection priority for Grammar Engine" }, + person: { type: "string", options: ["first", "second", "third"], when: "is_pronoun=true" }, + number: { type: "string", options: ["singular", "plural"], when: "is_pronoun=true" } + } + }, + + semantics: { + type: "object", + description: "Semantic constraints to ensure meaningful sentences", + critical: true, + fields: { + can_be_subject_type: { + type: "array", + options: ["human", "animal", "object", "food", "plant", "abstract", "place", "time"], + when: "is_verb=true", + description: "What types can be subject with this verb" + }, + can_take_object_type: { + type: "array", + options: ["human", "animal", "object", "food", "plant", "abstract", "place", "time"], + when: "verb_type=transitive", + description: "What types this verb can take as object" + }, + can_modify: { + type: "array", + options: ["action_verb", "stative_verb", "be_verb", "adjective", "adverb", "noun"], + when: "is_adv=true", + description: "What this adverb can modify" + }, + cannot_modify: { + type: "array", + options: ["action_verb", "stative_verb", "be_verb", "adjective", "adverb", "noun"], + when: "is_adv=true", + description: "What this adverb cannot modify" + }, + word_type: { + type: "string", + options: ["action", "state", "entity", "property", "concept", "relation"], + required: true, + description: "Semantic type of the word" + }, + is_countable: { + type: "boolean", + when: "is_object=true", + description: "Can this noun be counted" + }, + person_type: { + type: "string", + options: ["1st", "2nd", "3rd"], + when: "is_pronoun=true" + } + } + }, + + constraints: { + type: "object", + description: "Grammar constraints for word combination", + fields: { + followed_by: { + type: "string", + options: ["vowel_sound", "consonant_sound", "any"], + when: "is_article=true", + description: "What sound type must follow" + }, + match_subject: { + type: "object", + when: "is_be=true", + example: { "I": "am", "he": "is", "you": "are" }, + description: "Subject-verb agreement rules" + }, + match_with: { + type: "string", + description: "Must match with specific word type" + }, + position_rules: { + type: "array", + description: "Possible positions in sentence" + }, + requires_object: { + type: "boolean", + when: "verb_type=transitive" + } + } + } + } + }, + + form_keys_reference: { + verbs: { + v1: "Base form (eat, run)", + v_s_es: "Third person singular (eats, runs)", + v_ing: "Present participle (eating, running)", + v2: "Past simple (ate, ran)", + v3: "Past participle (eaten, run)" + }, + nouns: { + n_singular: "Singular form (cat, apple)", + n_plural: "Plural form (cats, apples)" + }, + adjectives: { + adj_base: "Base form (big, happy)", + adj_comparative: "Comparative (bigger, happier)", + adj_superlative: "Superlative (biggest, happiest)" + }, + adverbs: { + adv_base: "Base form (quickly, slowly)", + adv_comparative: "Comparative (more quickly)", + adv_superlative: "Superlative (most quickly)" + }, + pronouns: { + subject: "Subject pronoun (I, you, he)", + object: "Object pronoun (me, you, him)" + } + }, + + rules: { + vocab_code_format: "Must follow pattern: vocab-{3-digit-number}-{base_word}", + phonetic_format: "Use IPA notation enclosed in /slashes/", + audio_url_format: "Must be valid HTTPS URL pointing to MP3 or OGG file", + + semantic_compatibility: { + rule: "Ensure semantic constraints create meaningful sentences", + examples: { + valid: "human eats food (human can eat, food can be eaten)", + invalid: "table eats book (table cannot eat, book is not food)" + } + }, + + article_selection: { + rule: "Use phonetic to determine a/an", + algorithm: "Check first phoneme: if vowel sound use 'an', else use 'a'", + examples: { + an: "apple (/ˈæp.əl/ starts with /æ/)", + a: "cat (/kæt/ starts with /k/)" + } + }, + + be_verb_agreement: { + rule: "Match be verb form with subject", + mapping: { + "I": "am", + "you": "are", + "he/she/it": "is", + "we/they": "are" + } + } + }, + + examples: { + transitive_verb: { + vocab_code: "vocab-001-eat", + base_word: "eat", + translation: "ăn", + attributes: { + difficulty_score: 1, + category: "Action Verbs", + tags: ["verb", "action", "daily-routine"] + }, + forms: { + v1: { text: "eat", phonetic: "/iːt/", min_grade: 1 }, + v_s_es: { text: "eats", phonetic: "/iːts/", min_grade: 2 }, + v_ing: { text: "eating", phonetic: "/ˈiː.tɪŋ/", min_grade: 2 }, + v2: { text: "ate", phonetic: "/eɪt/", min_grade: 3 } + }, + syntax: { + is_verb: true, + verb_type: "transitive", + is_subject: false, + is_object: false + }, + semantics: { + can_be_subject_type: ["human", "animal"], + can_take_object_type: ["food", "plant"], + word_type: "action" + }, + constraints: { + requires_object: true + } + }, + + article: { + vocab_code: "vocab-art-01", + base_word: "a", + translation: "một (mạo từ bất định)", + attributes: { + difficulty_score: 1, + category: "Articles", + tags: ["article", "function-word", "grammar"] + }, + forms: { + base: { text: "a", phonetic: "/ə/", min_grade: 1 } + }, + syntax: { + is_article: true, + article_type: "indefinite", + priority: 1 + }, + constraints: { + followed_by: "consonant_sound" + } + }, + + adverb_manner: { + vocab_code: "vocab-adv-01", + base_word: "quickly", + translation: "nhanh chóng", + attributes: { + difficulty_score: 2, + category: "Adverbs", + tags: ["adverb", "manner"] + }, + forms: { + base: { text: "quickly", phonetic: "/ˈkwɪk.li/", min_grade: 2 } + }, + syntax: { + is_adv: true, + adv_type: "manner", + position: "after_verb" + }, + semantics: { + can_modify: ["action_verb"], + cannot_modify: ["stative_verb", "be_verb"], + word_type: "property" + } + }, + + noun: { + vocab_code: "vocab-200-apple", + base_word: "apple", + translation: "quả táo", + attributes: { + difficulty_score: 1, + category: "Nouns", + tags: ["noun", "food", "fruit"] + }, + forms: { + n_singular: { text: "apple", phonetic: "/ˈæp.əl/", min_grade: 1 }, + n_plural: { text: "apples", phonetic: "/ˈæp.əlz/", min_grade: 1 } + }, + syntax: { + is_subject: true, + is_object: true, + is_verb: false + }, + semantics: { + word_type: "entity", + is_countable: true + } + }, + + pronoun: { + vocab_code: "vocab-pron-01", + base_word: "I", + translation: "tôi", + attributes: { + difficulty_score: 1, + category: "Pronouns", + tags: ["pronoun", "personal"] + }, + forms: { + subject: { text: "I", phonetic: "/aɪ/", min_grade: 1 }, + object: { text: "me", phonetic: "/miː/", min_grade: 1 } + }, + syntax: { + is_subject: true, + is_pronoun: true, + pronoun_type: "personal", + person: "first", + number: "singular" + }, + semantics: { + person_type: "1st", + word_type: "entity" + }, + constraints: { + match_subject: { "I": "am" } + } + } + }, + + validation_checklist: { + before_submit: [ + "✓ vocab_code follows format vocab-XXX-{word}", + "✓ All phonetic notations use IPA format with /slashes/", + "✓ At least one form is defined in 'forms' object", + "✓ syntax object has at least one role set to true", + "✓ semantics.word_type is specified", + "✓ If is_verb=true, verb_type is specified", + "✓ If verb_type=transitive, can_take_object_type is specified", + "✓ If is_article=true, constraints.followed_by is specified", + "✓ If is_adv=true, can_modify array is specified" + ] + }, + + common_mistakes: [ + { + mistake: "Not setting any syntax role", + fix: "Set at least one is_{role} to true" + }, + { + mistake: "Using 'a' before vowel sound words", + fix: "Check phonetic - if starts with vowel sound, use 'an'" + }, + { + mistake: "Transitive verb without can_take_object_type", + fix: "Specify what types this verb can take as object" + }, + { + mistake: "Missing word_type in semantics", + fix: "Always specify: action, state, entity, property, concept, or relation" + }, + { + mistake: "Incorrect phonetic format", + fix: "Use IPA notation: /iːt/ not 'eet' or 'eat'" + } + ], + + ai_tips: { + efficiency: "Create related words together (eat, eats, eating) to maintain consistency", + accuracy: "Double-check phonetic transcription using Cambridge Dictionary or similar", + completeness: "Fill all relevant fields - more data means better Grammar Engine performance", + testing: "After creating words, test sentence generation to verify semantic constraints work", + documentation: "Use descriptive context_note in mappings to help future AI understand usage" + } + }; + + res.json({ + message: 'Vocabulary guide retrieved successfully', + data: guide + }); + + } catch (error) { + console.error('Error getting vocab guide:', error); + res.status(500).json({ + message: 'Error retrieving guide', + error: error.message + }); + } +}; + +// Helper functions + +/** + * Get complete vocabulary data with all relations + */ +async function getCompleteVocab(vocab_id) { + const vocab = await Vocab.findByPk(vocab_id, { + include: [ + { model: VocabMapping, as: 'mappings' }, + { model: VocabForm, as: 'forms' }, + { model: VocabRelation, as: 'relations' } + ] + }); + return formatVocabResponse(vocab); +} + +/** + * Format vocabulary response to match the expected structure + */ +function formatVocabResponse(vocab) { + const vocabJson = vocab.toJSON(); + + // Format attributes + const attributes = { + difficulty_score: vocabJson.difficulty_score, + category: vocabJson.category, + images: vocabJson.images || [], + tags: vocabJson.tags || [] + }; + + // Format forms as object keyed by form_key + const forms = {}; + if (vocabJson.forms) { + vocabJson.forms.forEach(form => { + forms[form.form_key] = { + text: form.text, + phonetic: form.phonetic, + audio: form.audio_url, + min_grade: form.min_grade, + description: form.description + }; + }); + } + + // Format relations grouped by type + const relations = { + synonyms: [], + antonyms: [], + related: [] + }; + if (vocabJson.relations) { + vocabJson.relations.forEach(rel => { + if (rel.relation_type === 'synonym') { + relations.synonyms.push(rel.related_word); + } else if (rel.relation_type === 'antonym') { + relations.antonyms.push(rel.related_word); + } else if (rel.relation_type === 'related') { + relations.related.push(rel.related_word); + } + }); + } + + return { + id: vocabJson.vocab_code, + vocab_id: vocabJson.vocab_id, + base_word: vocabJson.base_word, + translation: vocabJson.translation, + attributes, + mappings: vocabJson.mappings || [], + forms, + relations, + syntax: vocabJson.syntax || {}, + semantics: vocabJson.semantics || {}, + constraints: vocabJson.constraints || {} + }; +} diff --git a/data/moveup/g1/unit4.json b/data/moveup/g1/unit4.json new file mode 100644 index 0000000..330645b --- /dev/null +++ b/data/moveup/g1/unit4.json @@ -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"] + } + } +] \ No newline at end of file diff --git a/data/moveup/g1/unit5.json b/data/moveup/g1/unit5.json new file mode 100644 index 0000000..58410c0 --- /dev/null +++ b/data/moveup/g1/unit5.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g1/unit6.json b/data/moveup/g1/unit6.json new file mode 100644 index 0000000..8b17dd4 --- /dev/null +++ b/data/moveup/g1/unit6.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g2/unit4.json b/data/moveup/g2/unit4.json new file mode 100644 index 0000000..e64afe3 --- /dev/null +++ b/data/moveup/g2/unit4.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g2/unit5.json b/data/moveup/g2/unit5.json new file mode 100644 index 0000000..30a91d0 --- /dev/null +++ b/data/moveup/g2/unit5.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g2/unit6.json b/data/moveup/g2/unit6.json new file mode 100644 index 0000000..c57377b --- /dev/null +++ b/data/moveup/g2/unit6.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g3/unit10.json b/data/moveup/g3/unit10.json new file mode 100644 index 0000000..8083afd --- /dev/null +++ b/data/moveup/g3/unit10.json @@ -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" + } +] \ No newline at end of file diff --git a/data/moveup/g3/unit11.json b/data/moveup/g3/unit11.json new file mode 100644 index 0000000..45fb451 --- /dev/null +++ b/data/moveup/g3/unit11.json @@ -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" + } +] \ No newline at end of file diff --git a/data/moveup/g3/unit12.json b/data/moveup/g3/unit12.json new file mode 100644 index 0000000..5f9b2df --- /dev/null +++ b/data/moveup/g3/unit12.json @@ -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" + } +] \ No newline at end of file diff --git a/data/moveup/g3/unit7.json b/data/moveup/g3/unit7.json new file mode 100644 index 0000000..4bfd694 --- /dev/null +++ b/data/moveup/g3/unit7.json @@ -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" + } +] \ No newline at end of file diff --git a/data/moveup/g3/unit8.json b/data/moveup/g3/unit8.json new file mode 100644 index 0000000..35bc75e --- /dev/null +++ b/data/moveup/g3/unit8.json @@ -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" + } +] \ No newline at end of file diff --git a/data/moveup/g3/unit9.json b/data/moveup/g3/unit9.json new file mode 100644 index 0000000..1ed6003 --- /dev/null +++ b/data/moveup/g3/unit9.json @@ -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" + } +] \ No newline at end of file diff --git a/data/moveup/g4/unit10.json b/data/moveup/g4/unit10.json new file mode 100644 index 0000000..a9dc8a5 --- /dev/null +++ b/data/moveup/g4/unit10.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g4/unit11.json b/data/moveup/g4/unit11.json new file mode 100644 index 0000000..27140db --- /dev/null +++ b/data/moveup/g4/unit11.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g4/unit12.json b/data/moveup/g4/unit12.json new file mode 100644 index 0000000..63a7efe --- /dev/null +++ b/data/moveup/g4/unit12.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g4/unit7.json b/data/moveup/g4/unit7.json new file mode 100644 index 0000000..4f5a0d2 --- /dev/null +++ b/data/moveup/g4/unit7.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g4/unit8.json b/data/moveup/g4/unit8.json new file mode 100644 index 0000000..6fe37eb --- /dev/null +++ b/data/moveup/g4/unit8.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g4/unit9.json b/data/moveup/g4/unit9.json new file mode 100644 index 0000000..bd029c5 --- /dev/null +++ b/data/moveup/g4/unit9.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g5/unit10.json b/data/moveup/g5/unit10.json new file mode 100644 index 0000000..813f367 --- /dev/null +++ b/data/moveup/g5/unit10.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g5/unit11.json b/data/moveup/g5/unit11.json new file mode 100644 index 0000000..aa18990 --- /dev/null +++ b/data/moveup/g5/unit11.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g5/unit12.json b/data/moveup/g5/unit12.json new file mode 100644 index 0000000..cc01c4e --- /dev/null +++ b/data/moveup/g5/unit12.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g5/unit7.json b/data/moveup/g5/unit7.json new file mode 100644 index 0000000..85618d5 --- /dev/null +++ b/data/moveup/g5/unit7.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g5/unit8.json b/data/moveup/g5/unit8.json new file mode 100644 index 0000000..5433213 --- /dev/null +++ b/data/moveup/g5/unit8.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/data/moveup/g5/unit9.json b/data/moveup/g5/unit9.json new file mode 100644 index 0000000..799deea --- /dev/null +++ b/data/moveup/g5/unit9.json @@ -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" + ] + } + } +] \ No newline at end of file diff --git a/models/Grammar.js b/models/Grammar.js new file mode 100644 index 0000000..3db1b80 --- /dev/null +++ b/models/Grammar.js @@ -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; diff --git a/models/GrammarMapping.js b/models/GrammarMapping.js new file mode 100644 index 0000000..a6a16c3 --- /dev/null +++ b/models/GrammarMapping.js @@ -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; diff --git a/models/GrammarMediaStory.js b/models/GrammarMediaStory.js new file mode 100644 index 0000000..bd0e7bb --- /dev/null +++ b/models/GrammarMediaStory.js @@ -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; diff --git a/models/Lesson.js b/models/Lesson.js index 8403838..c728a7c 100644 --- a/models/Lesson.js +++ b/models/Lesson.js @@ -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 diff --git a/models/Story.js b/models/Story.js new file mode 100644 index 0000000..990628b --- /dev/null +++ b/models/Story.js @@ -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; diff --git a/models/Vocab.js b/models/Vocab.js new file mode 100644 index 0000000..5555b76 --- /dev/null +++ b/models/Vocab.js @@ -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; diff --git a/models/VocabForm.js b/models/VocabForm.js new file mode 100644 index 0000000..be53f8a --- /dev/null +++ b/models/VocabForm.js @@ -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; diff --git a/models/VocabMapping.js b/models/VocabMapping.js new file mode 100644 index 0000000..ba505b7 --- /dev/null +++ b/models/VocabMapping.js @@ -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; diff --git a/models/VocabRelation.js b/models/VocabRelation.js new file mode 100644 index 0000000..c75c605 --- /dev/null +++ b/models/VocabRelation.js @@ -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; diff --git a/models/index.js b/models/index.js index 97139d6..8f1258b 100644 --- a/models/index.js +++ b/models/index.js @@ -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, diff --git a/routes/grammarRoutes.js b/routes/grammarRoutes.js new file mode 100644 index 0000000..4b3a17c --- /dev/null +++ b/routes/grammarRoutes.js @@ -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; diff --git a/routes/learningContentRoutes.js b/routes/learningContentRoutes.js new file mode 100644 index 0000000..b95f9fb --- /dev/null +++ b/routes/learningContentRoutes.js @@ -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; diff --git a/routes/storyRoutes.js b/routes/storyRoutes.js new file mode 100644 index 0000000..5db5ba4 --- /dev/null +++ b/routes/storyRoutes.js @@ -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; diff --git a/routes/vocabRoutes.js b/routes/vocabRoutes.js new file mode 100644 index 0000000..9a2811f --- /dev/null +++ b/routes/vocabRoutes.js @@ -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; diff --git a/scripts/add-lesson-content-type-column.js b/scripts/add-lesson-content-type-column.js new file mode 100644 index 0000000..e1bcdba --- /dev/null +++ b/scripts/add-lesson-content-type-column.js @@ -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(); diff --git a/scripts/import-moveup-data.js b/scripts/import-moveup-data.js new file mode 100644 index 0000000..3854164 --- /dev/null +++ b/scripts/import-moveup-data.js @@ -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 };