539 lines
14 KiB
JavaScript
539 lines
14 KiB
JavaScript
const { Context, Vocab } = require('../models');
|
|
|
|
/**
|
|
* Context Controller - Workflow-based status management
|
|
* Status flow: 0 (Draft) -> 1 (Enriched) -> 2 (Prompt Ready) -> 3 (Generating) -> 4 (Image Ready) -> 5 (Approved)
|
|
*/
|
|
class ContextController {
|
|
/**
|
|
* Create new context - Status 0 (Draft)
|
|
* Required fields: title, desc, grade
|
|
*/
|
|
async createContext(req, res, next) {
|
|
try {
|
|
const { title, desc, grade, type, type_image , reference_id } = req.body;
|
|
|
|
// Validate required fields
|
|
if (!title || !desc || !grade) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Title, desc, and grade are required'
|
|
});
|
|
}
|
|
|
|
// Validate grade format (gradeX100 + unitX10 + lesson)
|
|
const gradeNum = parseInt(grade);
|
|
if (isNaN(gradeNum) || gradeNum < 100) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Grade must be in format: gradeX100 + unitX10 + lesson (e.g., 123 for Grade 1 Unit 2 Lesson 3)'
|
|
});
|
|
}
|
|
|
|
const context = await Context.create({
|
|
title,
|
|
desc,
|
|
grade: gradeNum,
|
|
type: type || 'general',
|
|
type_image: type_image || null,
|
|
status: 0, // Draft
|
|
context: '',
|
|
knowledge: '',
|
|
reference_id: reference_id || null
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Context created successfully',
|
|
data: context
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get contexts by status
|
|
*/
|
|
async getContextsByStatus(req, res, next) {
|
|
try {
|
|
const { status } = req.params;
|
|
const { page = 1, limit = 50, type, grade } = req.query;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const where = { status: parseInt(status) };
|
|
if (type) where.type = type;
|
|
if (grade) where.grade = parseInt(grade);
|
|
|
|
const { count, rows } = await Context.findAndCountAll({
|
|
where,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
order: [['created_at', 'DESC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
contexts: rows,
|
|
pagination: {
|
|
total: count,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
totalPages: Math.ceil(count / limit)
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update knowledge - Status 0 -> 1 (Enriched)
|
|
*/
|
|
async enrichContext(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { knowledge } = req.body;
|
|
|
|
if (!knowledge) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Knowledge is required'
|
|
});
|
|
}
|
|
|
|
const context = await Context.findByPk(id);
|
|
if (!context) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
if (context.status !== 0) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Context must be in Draft status (0) to enrich'
|
|
});
|
|
}
|
|
|
|
await context.update({
|
|
knowledge,
|
|
status: 1
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Context enriched successfully',
|
|
data: context
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update context and img_prompt - Status 1 -> 2 (Prompt Ready)
|
|
*/
|
|
async preparePrompt(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { context, img_prompt } = req.body;
|
|
|
|
if (!context || !img_prompt) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Context and img_prompt are required'
|
|
});
|
|
}
|
|
|
|
const contextRecord = await Context.findByPk(id);
|
|
if (!contextRecord) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
if (contextRecord.status !== 1) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Context must be in Enriched status (1) to prepare prompt'
|
|
});
|
|
}
|
|
|
|
await contextRecord.update({
|
|
context,
|
|
img_prompt,
|
|
status: 2
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Prompt prepared successfully',
|
|
data: contextRecord
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update status to 3 (Generating) or back to 1 (Enriched)
|
|
*/
|
|
async updateStatusFromPromptReady(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { status } = req.body;
|
|
|
|
const context = await Context.findByPk(id);
|
|
if (!context) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
await context.update({ status: parseInt(status) });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Status updated to ${status}`,
|
|
data: context
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk update all status 2 to status 3 (Prompt Ready -> Generating)
|
|
*/
|
|
async bulkUpdateStatus2To3(req, res, next) {
|
|
try {
|
|
const [affectedCount] = await Context.update(
|
|
{ status: 3 },
|
|
{ where: { status: 2 } }
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Updated ${affectedCount} context(s) from status 2 to status 3`,
|
|
data: {
|
|
affectedCount
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add images - Status 3 -> 4 (Image Ready)
|
|
*/
|
|
async addImages(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { image } = req.body;
|
|
|
|
if (!image || typeof image !== 'string' || image.trim().length === 0) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Image must be a non-empty string (URL)'
|
|
});
|
|
}
|
|
|
|
const context = await Context.findByPk(id);
|
|
if (!context) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
if (context.status !== 3) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Context must be in Generating status (3) to add images'
|
|
});
|
|
}
|
|
|
|
await context.update({
|
|
image,
|
|
status: 4
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Images added successfully',
|
|
data: context
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Approve context - Status 4 -> 5 (Approved)
|
|
*/
|
|
async approveContext(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const context = await Context.findByPk(id);
|
|
if (!context) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
if (context.status !== 4) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Context must be in Image Ready status (4) to approve'
|
|
});
|
|
}
|
|
// add image to Vocab Image
|
|
const currentVocab = await Vocab.findOne({ where: { vocab_id: context.reference_id } });
|
|
console.log('Current Vocab:', currentVocab);
|
|
if (currentVocab) {
|
|
if (context.type_image === 'small') {
|
|
const updatedImagesSmall = currentVocab.image_small || [];
|
|
updatedImagesSmall.push(context.image);
|
|
await currentVocab.update({ image_small: updatedImagesSmall });
|
|
} else if (context.type_image === 'square') {
|
|
const updatedImagesSquare = currentVocab.image_square || [];
|
|
updatedImagesSquare.push(context.image);
|
|
await currentVocab.update({ image_square: updatedImagesSquare });
|
|
} else if (context.type_image === 'normal') {
|
|
const updatedImagesNormal = currentVocab.image_normal || [];
|
|
updatedImagesNormal.push(context.image);
|
|
await currentVocab.update({ image_normal: updatedImagesNormal });
|
|
}
|
|
await currentVocab.save();
|
|
}
|
|
await context.update({
|
|
status: 5
|
|
});
|
|
res.json({
|
|
success: true,
|
|
message: 'Context approved successfully',
|
|
data: context
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get context by UUID (for viewing details)
|
|
*/
|
|
async getContextById(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
const context = await Context.findByPk(id);
|
|
|
|
if (!context) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: context
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all contexts with pagination and filters
|
|
*/
|
|
async getAllContexts(req, res, next) {
|
|
try {
|
|
const { page = 1, limit = 50, type, grade, status } = req.query;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const where = {};
|
|
if (type) where.type = type;
|
|
if (grade) where.grade = parseInt(grade);
|
|
if (status !== undefined) where.status = parseInt(status);
|
|
|
|
const { count, rows } = await Context.findAndCountAll({
|
|
where,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
order: [['created_at', 'DESC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
contexts: rows,
|
|
pagination: {
|
|
total: count,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
totalPages: Math.ceil(count / limit)
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search contexts with partial match on title/context + filter by type_image
|
|
*
|
|
* POST /api/contexts/search
|
|
* Body:
|
|
* {
|
|
* search : String - tìm kiếm một phần trong title HOẶC context (OR)
|
|
* title : String - tìm riêng trong title (LIKE)
|
|
* context_text : String - tìm riêng trong context (LIKE)
|
|
* type_image : String - exact match (e.g., 'small', 'square', 'normal')
|
|
* type : String - exact match
|
|
* status : Number - exact match
|
|
* grade : Number - exact match
|
|
* page : Number (default: 1)
|
|
* limit : Number (default: 50)
|
|
* }
|
|
*/
|
|
async searchContexts(req, res, next) {
|
|
try {
|
|
const {
|
|
search,
|
|
title,
|
|
context_text,
|
|
type_image,
|
|
type,
|
|
status,
|
|
grade,
|
|
page = 1,
|
|
limit = 50
|
|
} = req.body;
|
|
|
|
const { Op } = require('sequelize');
|
|
const offset = (page - 1) * limit;
|
|
const where = {};
|
|
|
|
// ── Exact-match filters ──────────────────────────────────────────────
|
|
if (type_image !== undefined && type_image !== null && type_image !== '') {
|
|
where.type_image = type_image;
|
|
}
|
|
if (type) where.type = type;
|
|
if (status !== undefined && status !== null) where.status = parseInt(status);
|
|
if (grade !== undefined && grade !== null) where.grade = parseInt(grade);
|
|
|
|
// ── Text search ──────────────────────────────────────────────────────
|
|
// `search` → title OR context (cả hai cùng lúc)
|
|
// `title` → chỉ title
|
|
// `context_text` → chỉ context
|
|
const textConditions = [];
|
|
|
|
if (search) {
|
|
textConditions.push(
|
|
{ title: { [Op.like]: `%${search}%` } },
|
|
{ context: { [Op.like]: `%${search}%` } }
|
|
);
|
|
}
|
|
if (title) {
|
|
textConditions.push({ title: { [Op.like]: `%${title}%` } });
|
|
}
|
|
if (context_text) {
|
|
textConditions.push({ context: { [Op.like]: `%${context_text}%` } });
|
|
}
|
|
|
|
if (textConditions.length > 0) {
|
|
where[Op.or] = textConditions;
|
|
}
|
|
|
|
const { count, rows } = await Context.findAndCountAll({
|
|
where,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
order: [['created_at', 'DESC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Search completed successfully',
|
|
data: {
|
|
contexts: rows,
|
|
pagination: {
|
|
total: count,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
totalPages: Math.ceil(count / limit)
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update context (general update - use with caution)
|
|
*/
|
|
async updateContext(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
const updates = req.body;
|
|
|
|
const context = await Context.findByPk(id);
|
|
if (!context) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
await context.update(updates);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Context updated successfully',
|
|
data: context
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete context
|
|
*/
|
|
async deleteContext(req, res, next) {
|
|
try {
|
|
const { id } = req.params;
|
|
const context = await Context.findByPk(id);
|
|
|
|
if (!context) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Context not found'
|
|
});
|
|
}
|
|
|
|
await context.destroy();
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Context deleted successfully'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new ContextController();
|