643 lines
17 KiB
JavaScript
643 lines
17 KiB
JavaScript
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
|
|
});
|
|
}
|
|
};
|