update context API
All checks were successful
Deploy to Production / deploy (push) Successful in 19s

This commit is contained in:
silverpro89
2026-02-06 11:28:06 +07:00
parent 97dbbd4d12
commit aaba22b40c
12 changed files with 1375 additions and 49 deletions

50
add-knowledge-column.js Normal file
View File

@@ -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);
}
})();

170
alter-context-table.js Normal file
View File

@@ -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();

16
check-columns.js Normal file
View File

@@ -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);
}
})();

18
cleanup-context.js Normal file
View File

@@ -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);
}
})();

View File

@@ -1,20 +1,67 @@
const { Context } = require('../models'); 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 { 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 { 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 offset = (page - 1) * limit;
const where = {}; const where = { status: parseInt(status) };
if (type) where.type = type; if (type) where.type = type;
if (title) where.title = title; if (grade) where.grade = parseInt(grade);
const { count, rows } = await Context.findAndCountAll({ const { count, rows } = await Context.findAndCountAll({
where, 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) { async getContextById(req, res, next) {
try { 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 { 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, success: true,
message: 'Context created successfully', data: {
data: context contexts: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / limit)
}
}
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -82,7 +361,7 @@ class ContextController {
} }
/** /**
* Update context * Update context (general update - use with caution)
*/ */
async updateContext(req, res, next) { async updateContext(req, res, next) {
try { try {

View File

@@ -15,11 +15,18 @@ const Context = sequelize.define('Context', {
}, },
context: { context: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: false, allowNull: true,
comment: 'Context description' 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: { knowledge: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true,
comment: 'Additional knowledge or information' comment: 'Additional knowledge or information'
}, },
type: { type: {
@@ -31,7 +38,7 @@ const Context = sequelize.define('Context', {
type: DataTypes.TEXT, type: DataTypes.TEXT,
comment: 'Detailed description or requirement' comment: 'Detailed description or requirement'
}, },
prompt: { img_prompt: {
type: DataTypes.JSON, type: DataTypes.JSON,
comment: 'Prompt configuration object' comment: 'Prompt configuration object'
}, },
@@ -39,30 +46,15 @@ const Context = sequelize.define('Context', {
type: DataTypes.JSON, type: DataTypes.JSON,
comment: 'Array of image URLs' comment: 'Array of image URLs'
}, },
difficulty: {
type: DataTypes.INTEGER,
defaultValue: 1,
comment: 'Difficulty level (1-10)'
},
max: { max: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
defaultValue: 0, defaultValue: 1,
comment: 'Maximum number of images or items' comment: 'Maximum number of images or items'
}, },
isPrompt: { status: {
type: DataTypes.BOOLEAN, type: DataTypes.INTEGER, // Hoặc DataTypes.ENUM('DRAFT', 'ENRICHED', 'PENDING_IMAGE', ...)
defaultValue: false, defaultValue: 0,
comment: 'Prompt created (0/1)' comment: '0: Draft, 1: Enriched, 2: Prompt_Ready, 3: Generating, 4: Image_Ready, 5: Approved'
},
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)'
}, },
created_at: { created_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
@@ -75,6 +67,7 @@ const Context = sequelize.define('Context', {
}, { }, {
tableName: 'context', tableName: 'context',
timestamps: true, timestamps: true,
underscored: false,
createdAt: 'created_at', createdAt: 'created_at',
updatedAt: 'updated_at', updatedAt: 'updated_at',
indexes: [ indexes: [

View File

@@ -24,7 +24,8 @@ const Vocab = sequelize.define('Vocab', {
// Ví dụ 111 là grade 1, unit 1, lesson 1 // Ví dụ 111 là grade 1, unit 1, lesson 1
location: { location: {
type: DataTypes.INTEGER, 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...) // Loại biến thể (V1, V2, V3, V_ing, Noun_Form...)
form_key: { form_key: {

708
public/contexts.html Normal file
View File

@@ -0,0 +1,708 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Context Workflow Manager</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
text-align: center;
color: white;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.workflow-info {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.workflow-info h3 {
color: #667eea;
margin-bottom: 15px;
}
.status-flow {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.status-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 15px;
border-radius: 20px;
font-size: 0.9em;
font-weight: bold;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.card h3 {
color: #667eea;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.status-indicator {
display: inline-block;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.85em;
font-weight: bold;
margin-left: 10px;
}
.status-0 { background: #ffd700; color: #000; }
.status-1 { background: #87ceeb; color: #000; }
.status-2 { background: #90ee90; color: #000; }
.status-3 { background: #ffa500; color: #fff; }
.status-4 { background: #9370db; color: #fff; }
.status-5 { background: #32cd32; color: #fff; }
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #333;
font-weight: 600;
}
input, textarea, select {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
min-height: 80px;
resize: vertical;
font-family: inherit;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 25px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
margin-top: 10px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
button:active {
transform: translateY(0);
}
.result {
margin-top: 15px;
padding: 15px;
border-radius: 5px;
background: #f8f9fa;
border-left: 4px solid #667eea;
display: none;
max-height: 400px;
overflow-y: auto;
}
.result.show {
display: block;
}
.result pre {
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
font-size: 12px;
}
.success {
border-left-color: #28a745;
background: #d4edda;
}
.error {
border-left-color: #dc3545;
background: #f8d7da;
}
.context-list {
margin-top: 15px;
}
.context-item {
background: #f8f9fa;
padding: 12px;
margin-bottom: 10px;
border-radius: 5px;
border-left: 4px solid #667eea;
cursor: pointer;
transition: background 0.2s;
}
.context-item:hover {
background: #e9ecef;
}
.context-item strong {
color: #667eea;
display: block;
margin-bottom: 5px;
}
.context-item small {
color: #666;
font-size: 0.85em;
}
.copy-btn {
background: #28a745;
padding: 5px 10px;
font-size: 12px;
display: inline-block;
width: auto;
margin: 5px 5px 0 0;
}
.selected-uuid {
background: #fff3cd;
border: 2px solid #ffc107;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
display: none;
}
.selected-uuid.show {
display: block;
}
.quick-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.quick-actions button {
flex: 1;
padding: 8px;
font-size: 12px;
}
.small-note {
font-size: 0.85em;
color: #666;
font-style: italic;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎯 Context Workflow Manager</h1>
<div class="workflow-info">
<h3>📊 Workflow Status Flow</h3>
<div class="status-flow">
<div class="status-badge">0: Draft</div>
<div class="status-badge">1: Enriched</div>
<div class="status-badge">2: Prompt Ready</div>
<div class="status-badge">3: Generating</div>
<div class="status-badge">4: Image Ready</div>
<div class="status-badge">5: Approved</div>
</div>
</div>
<!-- Status 0: Create Draft -->
<div class="grid">
<div class="card">
<h3>1⃣ Create Context <span class="status-indicator status-0">Status: 0</span></h3>
<div class="form-group">
<label>Title *</label>
<input type="text" id="createTitle" placeholder="e.g., Family Members">
</div>
<div class="form-group">
<label>Description *</label>
<textarea id="createDesc" placeholder="e.g., Learn vocabulary about family members"></textarea>
</div>
<div class="form-group">
<label>Grade * (Format: gradeX100 + unitX10 + lesson)</label>
<input type="number" id="createGrade" placeholder="e.g., 123 = Grade 1, Unit 2, Lesson 3">
<div class="small-note">Example: 123 = Grade 1, Unit 2, Lesson 3</div>
</div>
<div class="form-group">
<label>Type</label>
<input type="text" id="createType" value="vocabulary" placeholder="vocabulary, grammar, etc.">
</div>
<button onclick="createContext()">Create Context</button>
<div class="result" id="createResult"></div>
</div>
<div class="card">
<h3>📋 Get Contexts by Status <span class="status-indicator status-0">Status: 0</span></h3>
<button onclick="getContextsByStatus(0)">Get Status 0 (Draft)</button>
<div class="result" id="status0Result"></div>
<div class="context-list" id="status0List"></div>
</div>
</div>
<!-- Status 1: Enrich -->
<div class="grid">
<div class="card">
<h3>2⃣ Enrich Context <span class="status-indicator status-1">0 → 1</span></h3>
<div class="selected-uuid" id="enrichSelected"></div>
<div class="form-group">
<label>Context UUID *</label>
<input type="text" id="enrichUuid" placeholder="Paste UUID or click from list above">
</div>
<div class="form-group">
<label>Knowledge *</label>
<textarea id="enrichKnowledge" placeholder="Additional knowledge and information about this context"></textarea>
</div>
<button onclick="enrichContext()">Enrich (Status 0 → 1)</button>
<div class="result" id="enrichResult"></div>
</div>
<div class="card">
<h3>📋 Get Contexts by Status <span class="status-indicator status-1">Status: 1</span></h3>
<button onclick="getContextsByStatus(1)">Get Status 1 (Enriched)</button>
<div class="result" id="status1Result"></div>
<div class="context-list" id="status1List"></div>
</div>
</div>
<!-- Status 2: Prepare Prompt -->
<div class="grid">
<div class="card">
<h3>3⃣ Prepare Prompt <span class="status-indicator status-2">1 → 2</span></h3>
<div class="selected-uuid" id="promptSelected"></div>
<div class="form-group">
<label>Context UUID *</label>
<input type="text" id="promptUuid" placeholder="Paste UUID or click from list above">
</div>
<div class="form-group">
<label>Context Content *</label>
<textarea id="promptContext" placeholder="Main context content"></textarea>
</div>
<div class="form-group">
<label>Image Prompt * (JSON)</label>
<textarea id="imgPrompt" placeholder='{"prompt": "description", "style": "cartoon"}'></textarea>
<div class="small-note">Must be valid JSON object</div>
</div>
<button onclick="preparePrompt()">Prepare Prompt (Status 1 → 2)</button>
<div class="result" id="promptResult"></div>
</div>
<div class="card">
<h3>📋 Get Contexts by Status <span class="status-indicator status-2">Status: 2</span></h3>
<button onclick="getContextsByStatus(2)">Get Status 2 (Prompt Ready)</button>
<div class="result" id="status2Result"></div>
<div class="context-list" id="status2List"></div>
</div>
</div>
<!-- Status 3: Update Status -->
<div class="grid">
<div class="card">
<h3>4⃣ Update Status <span class="status-indicator status-3">2 → 3 or 1</span></h3>
<div class="selected-uuid" id="updateStatusSelected"></div>
<div class="form-group">
<label>Context UUID *</label>
<input type="text" id="updateStatusUuid" placeholder="Paste UUID or click from list above">
</div>
<div class="form-group">
<label>New Status *</label>
<select id="updateStatusValue">
<option value="3">3 - Generating</option>
<option value="1">1 - Back to Enriched</option>
</select>
</div>
<button onclick="updateStatus()">Update Status</button>
<div class="result" id="updateStatusResult"></div>
</div>
<div class="card">
<h3>📋 Get Contexts by Status <span class="status-indicator status-3">Status: 3</span></h3>
<button onclick="getContextsByStatus(3)">Get Status 3 (Generating)</button>
<div class="result" id="status3Result"></div>
<div class="context-list" id="status3List"></div>
</div>
</div>
<!-- Status 4: Add Images -->
<div class="grid">
<div class="card">
<h3>5⃣ Add Images <span class="status-indicator status-4">3 → 4</span></h3>
<div class="selected-uuid" id="imagesSelected"></div>
<div class="form-group">
<label>Context UUID *</label>
<input type="text" id="imagesUuid" placeholder="Paste UUID or click from list above">
</div>
<div class="form-group">
<label>Image URLs * (JSON Array)</label>
<textarea id="imageUrls" placeholder='["https://example.com/image1.jpg", "https://example.com/image2.jpg"]'></textarea>
<div class="small-note">Must be valid JSON array of URLs</div>
</div>
<button onclick="addImages()">Add Images (Status 3 → 4)</button>
<div class="result" id="imagesResult"></div>
</div>
<div class="card">
<h3>📋 Get Contexts by Status <span class="status-indicator status-4">Status: 4</span></h3>
<button onclick="getContextsByStatus(4)">Get Status 4 (Image Ready)</button>
<div class="result" id="status4Result"></div>
<div class="context-list" id="status4List"></div>
</div>
</div>
<!-- Status 5: Approve -->
<div class="grid">
<div class="card">
<h3>6⃣ Approve Context <span class="status-indicator status-5">4 → 5</span></h3>
<div class="selected-uuid" id="approveSelected"></div>
<div class="form-group">
<label>Context UUID *</label>
<input type="text" id="approveUuid" placeholder="Paste UUID or click from list above">
</div>
<button onclick="approveContext()">Approve (Status 4 → 5)</button>
<div class="result" id="approveResult"></div>
</div>
<div class="card">
<h3>📋 Get Contexts by Status <span class="status-indicator status-5">Status: 5</span></h3>
<button onclick="getContextsByStatus(5)">Get Status 5 (Approved)</button>
<div class="result" id="status5Result"></div>
<div class="context-list" id="status5List"></div>
</div>
</div>
</div>
<script>
const API_BASE = '/api/contexts';
function getHeaders() {
return {
'Content-Type': 'application/json'
};
}
function showResult(elementId, data, isSuccess = true) {
const element = document.getElementById(elementId);
element.className = `result show ${isSuccess ? 'success' : 'error'}`;
element.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
}
function copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(() => {
const originalText = button.textContent;
button.textContent = '✓ Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
});
}
function selectUuid(uuid, targetInputId, selectedDivId) {
document.getElementById(targetInputId).value = uuid;
const selectedDiv = document.getElementById(selectedDivId);
selectedDiv.innerHTML = `<strong>Selected UUID:</strong> ${uuid}`;
selectedDiv.className = 'selected-uuid show';
// Scroll to the input
document.getElementById(targetInputId).scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async function createContext() {
const headers = getHeaders();
const title = document.getElementById('createTitle').value.trim();
const desc = document.getElementById('createDesc').value.trim();
const grade = document.getElementById('createGrade').value.trim();
const type = document.getElementById('createType').value.trim();
if (!title || !desc || !grade) {
showResult('createResult', { error: 'Title, desc, and grade are required' }, false);
return;
}
try {
const response = await fetch(API_BASE, {
method: 'POST',
headers: headers,
body: JSON.stringify({ title, desc, grade: parseInt(grade), type })
});
const data = await response.json();
showResult('createResult', data, response.ok);
if (response.ok) {
// Auto refresh status 0 list
getContextsByStatus(0);
}
} catch (error) {
showResult('createResult', { error: error.message }, false);
}
}
async function getContextsByStatus(status) {
const headers = getHeaders();
try {
const response = await fetch(`${API_BASE}/status/${status}`, {
headers: headers
});
const data = await response.json();
showResult(`status${status}Result`, data, response.ok);
if (response.ok && data.data && data.data.contexts) {
displayContextList(data.data.contexts, status);
}
} catch (error) {
showResult(`status${status}Result`, { error: error.message }, false);
}
}
function displayContextList(contexts, status) {
const listElement = document.getElementById(`status${status}List`);
if (contexts.length === 0) {
listElement.innerHTML = '<p style="color: #666; font-style: italic;">No contexts found</p>';
return;
}
listElement.innerHTML = contexts.map(ctx => `
<div class="context-item">
<strong>${ctx.title}</strong>
<small>UUID: ${ctx.uuid} | Grade: ${ctx.grade} | Type: ${ctx.type}</small>
<div class="quick-actions">
<button class="copy-btn" onclick="copyToClipboard('${ctx.uuid}', this)">📋 Copy UUID</button>
${getStatusActionButton(ctx, status)}
</div>
</div>
`).join('');
}
function getStatusActionButton(ctx, status) {
const uuid = ctx.uuid;
switch(status) {
case 0:
return `<button class="copy-btn" style="background: #17a2b8;" onclick="selectUuid('${uuid}', 'enrichUuid', 'enrichSelected')">→ Enrich</button>`;
case 1:
return `<button class="copy-btn" style="background: #17a2b8;" onclick="selectUuid('${uuid}', 'promptUuid', 'promptSelected')">→ Prepare Prompt</button>`;
case 2:
return `<button class="copy-btn" style="background: #17a2b8;" onclick="selectUuid('${uuid}', 'updateStatusUuid', 'updateStatusSelected')">→ Update Status</button>`;
case 3:
return `<button class="copy-btn" style="background: #17a2b8;" onclick="selectUuid('${uuid}', 'imagesUuid', 'imagesSelected')">→ Add Images</button>`;
case 4:
return `<button class="copy-btn" style="background: #17a2b8;" onclick="selectUuid('${uuid}', 'approveUuid', 'approveSelected')">→ Approve</button>`;
default:
return '';
}
}
async function enrichContext() {
const headers = getHeaders();
const uuid = document.getElementById('enrichUuid').value.trim();
const knowledge = document.getElementById('enrichKnowledge').value.trim();
if (!uuid || !knowledge) {
showResult('enrichResult', { error: 'UUID and knowledge are required' }, false);
return;
}
try {
const response = await fetch(`${API_BASE}/${uuid}/enrich`, {
method: 'POST',
headers: headers,
body: JSON.stringify({ knowledge })
});
const data = await response.json();
showResult('enrichResult', data, response.ok);
if (response.ok) {
getContextsByStatus(1);
}
} catch (error) {
showResult('enrichResult', { error: error.message }, false);
}
}
async function preparePrompt() {
const headers = getHeaders();
const uuid = document.getElementById('promptUuid').value.trim();
const context = document.getElementById('promptContext').value.trim();
const imgPromptStr = document.getElementById('imgPrompt').value.trim();
if (!uuid || !context || !imgPromptStr) {
showResult('promptResult', { error: 'UUID, context, and img_prompt are required' }, false);
return;
}
try {
const img_prompt = JSON.parse(imgPromptStr);
const response = await fetch(`${API_BASE}/${uuid}/prepare-prompt`, {
method: 'POST',
headers: headers,
body: JSON.stringify({ context, img_prompt })
});
const data = await response.json();
showResult('promptResult', data, response.ok);
if (response.ok) {
getContextsByStatus(2);
}
} catch (error) {
showResult('promptResult', { error: error.message }, false);
}
}
async function updateStatus() {
const headers = getHeaders();
const uuid = document.getElementById('updateStatusUuid').value.trim();
const status = document.getElementById('updateStatusValue').value;
if (!uuid) {
showResult('updateStatusResult', { error: 'UUID is required' }, false);
return;
}
try {
const response = await fetch(`${API_BASE}/${uuid}/update-status`, {
method: 'POST',
headers: headers,
body: JSON.stringify({ status: parseInt(status) })
});
const data = await response.json();
showResult('updateStatusResult', data, response.ok);
if (response.ok) {
getContextsByStatus(3);
}
} catch (error) {
showResult('updateStatusResult', { error: error.message }, false);
}
}
async function addImages() {
const headers = getHeaders();
const uuid = document.getElementById('imagesUuid').value.trim();
const imageUrlsStr = document.getElementById('imageUrls').value.trim();
if (!uuid || !imageUrlsStr) {
showResult('imagesResult', { error: 'UUID and image URLs are required' }, false);
return;
}
try {
const image = JSON.parse(imageUrlsStr);
if (!Array.isArray(image)) {
throw new Error('Image URLs must be an array');
}
const response = await fetch(`${API_BASE}/${uuid}/add-images`, {
method: 'POST',
headers: headers,
body: JSON.stringify({ image })
});
const data = await response.json();
showResult('imagesResult', data, response.ok);
if (response.ok) {
getContextsByStatus(4);
}
} catch (error) {
showResult('imagesResult', { error: error.message }, false);
}
}
async function approveContext() {
const headers = getHeaders();
const uuid = document.getElementById('approveUuid').value.trim();
if (!uuid) {
showResult('approveResult', { error: 'UUID is required' }, false);
return;
}
try {
const response = await fetch(`${API_BASE}/${uuid}/approve`, {
method: 'POST',
headers: headers
});
const data = await response.json();
showResult('approveResult', data, response.ok);
if (response.ok) {
getContextsByStatus(5);
}
} catch (error) {
showResult('approveResult', { error: error.message }, false);
}
}
// Page loaded
window.addEventListener('load', () => {
console.log('Context Workflow Manager loaded');
});
</script>
</body>
</html>

27
rename-img-prompt.js Normal file
View File

@@ -0,0 +1,27 @@
const {sequelize} = require('./config/database');
(async () => {
try {
await sequelize.authenticate();
console.log('✅ Database connected');
// Rename promptForImage to img_prompt
await sequelize.query(`
ALTER TABLE context
CHANGE COLUMN promptForImage img_prompt JSON
COMMENT 'Prompt configuration object'
`);
console.log('✅ Renamed promptForImage to img_prompt');
// Show final structure
const [cols] = await sequelize.query('DESCRIBE context');
console.log('\n📊 Final Context table structure:');
cols.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);
}
})();

View File

@@ -4,22 +4,41 @@ const contextController = require('../controllers/contextController');
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
/** /**
* Context Routes * Context Routes - Workflow-based
* Status: 0 (Draft) -> 1 (Enriched) -> 2 (Prompt Ready) -> 3 (Generating) -> 4 (Image Ready) -> 5 (Approved)
*/ */
// Get all contexts // Create new context (Title, Desc, Grade) - Status 0
router.get('/', authenticateToken, contextController.getAllContexts); router.post('/', contextController.createContext);
// Get contexts by status
router.get('/status/:status', contextController.getContextsByStatus);
// Status 0 -> 1: Update knowledge
router.post('/:id/enrich', contextController.enrichContext);
// Status 1 -> 2: Update context and img_prompt
router.post('/:id/prepare-prompt', contextController.preparePrompt);
// Status 2 -> 3 or 1: Update status
router.post('/:id/update-status', contextController.updateStatusFromPromptReady);
// Status 3 -> 4: Add images
router.post('/:id/add-images', contextController.addImages);
// Status 4 -> 5: Approve
router.post('/:id/approve', contextController.approveContext);
// Get all contexts (with optional filters)
router.get('/', contextController.getAllContexts);
// Get context by UUID // Get context by UUID
router.get('/:id', authenticateToken, contextController.getContextById); router.get('/:id', contextController.getContextById);
// Create context // Update context (general - use with caution)
router.post('/', authenticateToken, contextController.createContext); router.put('/:id', contextController.updateContext);
// Update context
router.put('/:id', authenticateToken, contextController.updateContext);
// Delete context // Delete context
router.delete('/:id', authenticateToken, contextController.deleteContext); router.delete('/:id', contextController.deleteContext);
module.exports = router; module.exports = router;

45
sync-context-alter.js Normal file
View File

@@ -0,0 +1,45 @@
/**
* Sync Context model to database with ALTER
* This will update the existing table structure
*/
const { sequelize } = require('./config/database');
const Context = require('./models/Context');
async function syncContext() {
try {
console.log('🔄 Starting Context table sync with ALTER...');
// Test connection
await sequelize.authenticate();
console.log('✅ Database connection OK');
// Sync Context model with alter to update structure
await Context.sync({ alter: true });
console.log('✅ Context table synced successfully (grade column updated to INTEGER)');
// Show table structure
const [columns] = await sequelize.query(`
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'context' AND TABLE_SCHEMA = DATABASE()
ORDER BY ORDINAL_POSITION
`);
console.log('\n📊 Context table structure:');
columns.forEach((col, index) => {
console.log(` ${index + 1}. ${col.COLUMN_NAME} - ${col.COLUMN_TYPE} ${col.IS_NULLABLE === 'NO' ? 'NOT NULL' : 'NULL'}`);
if (col.COLUMN_COMMENT) {
console.log(` Comment: ${col.COLUMN_COMMENT}`);
}
});
console.log('\n✅ Context table update complete!');
process.exit(0);
} catch (error) {
console.error('❌ Error syncing Context table:', error.message);
console.error(error.stack);
process.exit(1);
}
}
syncContext();

View File

@@ -17,8 +17,8 @@ async function syncDatabase() {
setupRelationships(); setupRelationships();
console.log('✅ Model relationships configured'); console.log('✅ Model relationships configured');
// Sync all models (creates tables if not exist) // Sync all models (creates tables if not exist, alters existing tables)
await sequelize.sync({ alter: false, force: false }); await sequelize.sync({ alter: true, force: false });
console.log('✅ All models synced successfully'); console.log('✅ All models synced successfully');
// Show created tables // Show created tables