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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff