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,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
});
}
};