diff --git a/add-knowledge-column.js b/add-knowledge-column.js new file mode 100644 index 0000000..c0fe206 --- /dev/null +++ b/add-knowledge-column.js @@ -0,0 +1,50 @@ +const {sequelize} = require('./config/database'); + +(async () => { + try { + await sequelize.authenticate(); + console.log('β Database connected'); + + // Check current columns + const [cols] = await sequelize.query('DESCRIBE context'); + console.log('\nπ Current columns:'); + cols.forEach(c => console.log(` - ${c.Field}`)); + + const columnNames = cols.map(c => c.Field); + + // Add knowledge column if not exists + if (!columnNames.includes('knowledge')) { + await sequelize.query(` + ALTER TABLE context + ADD COLUMN knowledge TEXT NULL + COMMENT 'Additional knowledge or information' + `); + console.log('\nβ Added knowledge column'); + } else { + console.log('\nβ knowledge column already exists'); + } + + // Add status column if not exists + if (!columnNames.includes('status')) { + await sequelize.query(` + ALTER TABLE context + ADD COLUMN status INT DEFAULT 0 + COMMENT '0: Draft, 1: Enriched, 2: Prompt_Ready, 3: Generating, 4: Image_Ready, 5: Approved' + `); + console.log('β Added status column'); + } else { + console.log('β status column already exists'); + } + + // Show final structure + const [finalCols] = await sequelize.query('DESCRIBE context'); + console.log('\nπ Final Context table structure:'); + finalCols.forEach((c, i) => console.log(` ${i+1}. ${c.Field} (${c.Type})`)); + + process.exit(0); + } catch (error) { + console.error('β Error:', error.message); + console.error(error.stack); + process.exit(1); + } +})(); diff --git a/alter-context-table.js b/alter-context-table.js new file mode 100644 index 0000000..9a232e3 --- /dev/null +++ b/alter-context-table.js @@ -0,0 +1,170 @@ +/** + * Alter context table to update grade column and add/update other fields + */ +const { sequelize } = require('./config/database'); + +async function alterContextTable() { + try { + console.log('π Starting context table alteration...'); + + // Test connection first + await sequelize.authenticate(); + console.log('β Database connection OK'); + + // Drop old columns if they exist + try { + await sequelize.query(`ALTER TABLE context DROP COLUMN IF EXISTS difficulty`); + console.log('β Dropped old difficulty column'); + } catch (error) { + console.log('βΉοΈ difficulty column might not exist'); + } + + try { + await sequelize.query(`ALTER TABLE context DROP COLUMN IF EXISTS isPrompt`); + console.log('β Dropped old isPrompt column'); + } catch (error) { + console.log('βΉοΈ isPrompt column might not exist'); + } + + try { + await sequelize.query(`ALTER TABLE context DROP COLUMN IF EXISTS isList`); + console.log('β Dropped old isList column'); + } catch (error) { + console.log('βΉοΈ isList column might not exist'); + } + + try { + await sequelize.query(`ALTER TABLE context DROP COLUMN IF EXISTS isApprove`); + console.log('β Dropped old isApprove column'); + } catch (error) { + console.log('βΉοΈ isApprove column might not exist'); + } + + try { + await sequelize.query(`ALTER TABLE context DROP COLUMN IF EXISTS prompt`); + console.log('β Dropped old prompt column'); + } catch (error) { + console.log('βΉοΈ prompt column might not exist'); + } + + // Modify grade column (change from VARCHAR to INT) + try { + await sequelize.query(` + ALTER TABLE context + MODIFY COLUMN grade INT NOT NULL DEFAULT 100 + COMMENT 'It is number of gradeX100 + unitX10 + lesson (e.g., Grade 1 Unit 2 Lesson 3 = 123)' + `); + console.log('β Modified grade column to INT'); + } catch (error) { + console.log('β οΈ Grade column modification error:', error.message); + // Try adding if not exists + try { + await sequelize.query(` + ALTER TABLE context + ADD COLUMN grade INT NOT NULL DEFAULT 100 + COMMENT 'It is number of gradeX100 + unitX10 + lesson (e.g., Grade 1 Unit 2 Lesson 3 = 123)' + `); + console.log('β Added grade column'); + } catch (addError) { + console.log('β οΈ Could not add grade column:', addError.message); + } + } + + // Modify knowledge column + try { + await sequelize.query(` + ALTER TABLE context + MODIFY COLUMN knowledge TEXT DEFAULT '' + COMMENT 'Additional knowledge or information' + `); + console.log('β Modified knowledge column'); + } catch (error) { + console.log('βΉοΈ Knowledge column might already be correct'); + } + + // Rename prompt to promptForImage if needed + try { + await sequelize.query(` + ALTER TABLE context + CHANGE COLUMN prompt promptForImage JSON + COMMENT 'Prompt configuration object' + `); + console.log('β Renamed prompt to promptForImage'); + } catch (error) { + console.log('βΉοΈ Column might already be named promptForImage'); + } + + // Modify promptForImage if it exists + try { + await sequelize.query(` + ALTER TABLE context + MODIFY COLUMN promptForImage JSON + COMMENT 'Prompt configuration object' + `); + console.log('β Modified promptForImage column'); + } catch (error) { + // Try adding if not exists + try { + await sequelize.query(` + ALTER TABLE context + ADD COLUMN promptForImage JSON + COMMENT 'Prompt configuration object' + `); + console.log('β Added promptForImage column'); + } catch (addError) { + console.log('βΉοΈ promptForImage column might already exist'); + } + } + + // Modify max column + try { + await sequelize.query(` + ALTER TABLE context + MODIFY COLUMN max INT DEFAULT 1 + COMMENT 'Maximum number of images or items' + `); + console.log('β Modified max column'); + } catch (error) { + console.log('βΉοΈ max column might already be correct'); + } + + // Add status column if not exists + try { + await sequelize.query(` + ALTER TABLE context + ADD COLUMN status INT DEFAULT 0 + COMMENT '0: Draft, 1: Enriched, 2: Prompt_Ready, 3: Generating, 4: Image_Ready, 5: Approved' + `); + console.log('β Added status column'); + } catch (error) { + console.log('βΉοΈ status column might already exist'); + // Try modifying if exists + try { + await sequelize.query(` + ALTER TABLE context + MODIFY COLUMN status INT DEFAULT 0 + COMMENT '0: Draft, 1: Enriched, 2: Prompt_Ready, 3: Generating, 4: Image_Ready, 5: Approved' + `); + console.log('β Modified status column'); + } catch (modError) { + console.log('βΉοΈ Status column might already be correct'); + } + } + + // Show final structure + const [columns] = await sequelize.query('DESCRIBE context'); + console.log('\nπ Context table structure:'); + columns.forEach((col, index) => { + console.log(` ${index + 1}. ${col.Field} (${col.Type}) ${col.Null === 'NO' ? 'NOT NULL' : 'NULL'} ${col.Default ? `DEFAULT ${col.Default}` : ''}`); + }); + + console.log('\nβ Context table alteration complete!'); + process.exit(0); + } catch (error) { + console.error('β Error altering context table:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +alterContextTable(); diff --git a/check-columns.js b/check-columns.js new file mode 100644 index 0000000..da4fec1 --- /dev/null +++ b/check-columns.js @@ -0,0 +1,16 @@ +const {sequelize} = require('./config/database'); + +(async () => { + try { + await sequelize.authenticate(); + const [cols] = await sequelize.query('DESCRIBE context'); + console.log('\nπ Context table columns:'); + cols.forEach((c, i) => { + console.log(` ${i+1}. ${c.Field} (${c.Type})`); + }); + process.exit(0); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +})(); diff --git a/cleanup-context.js b/cleanup-context.js new file mode 100644 index 0000000..0df9aaf --- /dev/null +++ b/cleanup-context.js @@ -0,0 +1,18 @@ +const {sequelize} = require('./config/database'); + +(async () => { + try { + await sequelize.authenticate(); + await sequelize.query('ALTER TABLE context DROP COLUMN is_prompt, DROP COLUMN is_list, DROP COLUMN is_approve'); + console.log('β Dropped old columns'); + + const [cols] = await sequelize.query('DESCRIBE context'); + console.log('\nπ Final Context table:'); + cols.forEach(c => console.log(` ${c.Field} (${c.Type})`)); + + process.exit(0); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +})(); diff --git a/controllers/contextController.js b/controllers/contextController.js index f6bb63f..a145e4b 100644 --- a/controllers/contextController.js +++ b/controllers/contextController.js @@ -1,20 +1,67 @@ const { Context } = require('../models'); /** - * Context Controller + * Context Controller - Workflow-based status management + * Status flow: 0 (Draft) -> 1 (Enriched) -> 2 (Prompt Ready) -> 3 (Generating) -> 4 (Image Ready) -> 5 (Approved) */ class ContextController { /** - * Get all contexts with pagination and filters + * Create new context - Status 0 (Draft) + * Required fields: title, desc, grade */ - async getAllContexts(req, res, next) { + async createContext(req, res, next) { try { - const { page = 1, limit = 50, type, title } = req.query; + const { title, desc, grade, type } = 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', + status: 0, // Draft + context: '', + knowledge: '' + }); + + 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 = {}; + const where = { status: parseInt(status) }; if (type) where.type = type; - if (title) where.title = title; + if (grade) where.grade = parseInt(grade); const { count, rows } = await Context.findAndCountAll({ where, @@ -41,7 +88,219 @@ class ContextController { } /** - * Get context by UUID + * 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; + + if (![1, 3].includes(parseInt(status))) { + return res.status(400).json({ + success: false, + message: 'Status must be 1 (Enriched) or 3 (Generating)' + }); + } + + const context = await Context.findByPk(id); + if (!context) { + return res.status(404).json({ + success: false, + message: 'Context not found' + }); + } + + if (context.status !== 2) { + return res.status(400).json({ + success: false, + message: 'Context must be in Prompt Ready status (2) to update' + }); + } + + await context.update({ status: parseInt(status) }); + + res.json({ + success: true, + message: `Status updated to ${status}`, + data: context + }); + } 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 || !Array.isArray(image) || image.length === 0) { + return res.status(400).json({ + success: false, + message: 'Image must be a non-empty array of URLs' + }); + } + + 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' + }); + } + + 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 { @@ -65,16 +324,36 @@ class ContextController { } /** - * Create new context + * Get all contexts with pagination and filters */ - async createContext(req, res, next) { + async getAllContexts(req, res, next) { try { - const context = await Context.create(req.body); + const { page = 1, limit = 50, type, grade, status } = req.query; + const offset = (page - 1) * limit; - res.status(201).json({ + 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, - message: 'Context created successfully', - data: context + data: { + contexts: rows, + pagination: { + total: count, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(count / limit) + } + } }); } catch (error) { next(error); @@ -82,7 +361,7 @@ class ContextController { } /** - * Update context + * Update context (general update - use with caution) */ async updateContext(req, res, next) { try { diff --git a/models/Context.js b/models/Context.js index b56f18a..75aa618 100644 --- a/models/Context.js +++ b/models/Context.js @@ -15,11 +15,18 @@ const Context = sequelize.define('Context', { }, context: { type: DataTypes.TEXT, - allowNull: false, + allowNull: true, comment: 'Context description' }, + grade : { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100, + comment: 'It is number of gradeX100 + unitX10 + lesson (e.g., Grade 1 Unit 2 Lesson 3 = 123)' + }, knowledge: { type: DataTypes.TEXT, + allowNull: true, comment: 'Additional knowledge or information' }, type: { @@ -31,7 +38,7 @@ const Context = sequelize.define('Context', { type: DataTypes.TEXT, comment: 'Detailed description or requirement' }, - prompt: { + img_prompt: { type: DataTypes.JSON, comment: 'Prompt configuration object' }, @@ -39,30 +46,15 @@ const Context = sequelize.define('Context', { type: DataTypes.JSON, comment: 'Array of image URLs' }, - difficulty: { - type: DataTypes.INTEGER, - defaultValue: 1, - comment: 'Difficulty level (1-10)' - }, max: { type: DataTypes.INTEGER, - defaultValue: 0, + defaultValue: 1, comment: 'Maximum number of images or items' }, - isPrompt: { - type: DataTypes.BOOLEAN, - defaultValue: false, - comment: 'Prompt created (0/1)' - }, - isList: { - type: DataTypes.BOOLEAN, - defaultValue: false, - comment: 'Waiting for more images (0/1)' - }, - isApprove: { - type: DataTypes.BOOLEAN, - defaultValue: false, - comment: 'Teacher approval status (0/1)' + status: { + type: DataTypes.INTEGER, // HoαΊ·c DataTypes.ENUM('DRAFT', 'ENRICHED', 'PENDING_IMAGE', ...) + defaultValue: 0, + comment: '0: Draft, 1: Enriched, 2: Prompt_Ready, 3: Generating, 4: Image_Ready, 5: Approved' }, created_at: { type: DataTypes.DATE, @@ -75,6 +67,7 @@ const Context = sequelize.define('Context', { }, { tableName: 'context', timestamps: true, + underscored: false, createdAt: 'created_at', updatedAt: 'updated_at', indexes: [ diff --git a/models/Vocab.js b/models/Vocab.js index a6e1c42..7d1a3b2 100644 --- a/models/Vocab.js +++ b/models/Vocab.js @@ -24,7 +24,8 @@ const Vocab = sequelize.define('Vocab', { // VΓ dα»₯ 111 lΓ grade 1, unit 1, lesson 1 location: { type: DataTypes.INTEGER, - comment: 'Location or source of the vocabulary' + defaultValue: 100, + comment: 'It is number of gradeX100 + unitX10 + lesson (e.g., Grade 1 Unit 2 Lesson 3 = 123)' }, // LoαΊ‘i biαΊΏn thα» (V1, V2, V3, V_ing, Noun_Form...) form_key: { diff --git a/public/contexts.html b/public/contexts.html new file mode 100644 index 0000000..acc8f95 --- /dev/null +++ b/public/contexts.html @@ -0,0 +1,708 @@ + + +
+ + +