feat(v3): restructure project + add complete frontend

- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 907a5c0e92
331 changed files with 23511 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from . import models
from . import services
from . import controllers

View File

@@ -0,0 +1,20 @@
{
'name': 'EnCoach AI Course',
'version': '19.0.1.0',
'category': 'Education',
'summary': 'AI content generation pipelines for General English and IELTS courses',
'author': 'EnCoach',
'license': 'LGPL-3',
'depends': ['encoach_core', 'encoach_exam_template', 'encoach_course_gen'],
'data': [
'security/ir.model.access.csv',
'views/ai_generation_log_views.xml',
'views/ai_ielts_log_views.xml',
'views/ai_course_menus.xml',
'views/ai_review_views.xml',
'views/ielts_review_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
}

View File

@@ -0,0 +1 @@
from . import ai_course

View File

@@ -0,0 +1,396 @@
import json
import logging
from odoo import http, fields
from odoo.http import request
from odoo.addons.encoach_api.controllers.base import (
jwt_required, _json_response, _error_response, _get_json_body, _paginate
)
_logger = logging.getLogger(__name__)
class EncoachAiCourseController(http.Controller):
# ------------------------------------------------------------------
# POST /api/ai-course/english/create
# ------------------------------------------------------------------
@http.route('/api/ai-course/english/create', type='http', auth='none',
methods=['POST'], csrf=False)
@jwt_required
def create_english(self, **kw):
try:
body = _get_json_body()
cefr_level = body.get('cefr_level')
if not cefr_level:
return _error_response('cefr_level is required', 400)
user = request.env.user.sudo()
gap_profile_id = body.get('gap_profile_id')
GapProfile = request.env['encoach.gap.profile'].sudo()
if gap_profile_id:
gap_profile = GapProfile.browse(int(gap_profile_id))
if not gap_profile.exists():
return _error_response('Gap profile not found', 404)
else:
gap_profile = GapProfile.search(
[('student_id', '=', user.id)],
order='created_at desc', limit=1,
)
if not gap_profile:
gap_profile = GapProfile.create({
'student_id': user.id,
'source_type': 'exam',
'skill_gaps': json.dumps([]),
'topic_weaknesses': json.dumps([]),
})
brief = {
'skill_gaps': json.loads(gap_profile.skill_gaps or '[]'),
'topic_weaknesses': json.loads(gap_profile.topic_weaknesses or '[]'),
'cefr_level': cefr_level,
}
Log = request.env['encoach.ai.generation.log'].sudo()
log = Log.create({
'student_id': user.id,
'course_type': 'general_english',
'status': 'pending_review',
'brief': json.dumps(brief),
'attempts': 1,
})
return _json_response({
'log_id': log.id,
'status': log.status,
'brief': brief,
}, 201)
except Exception as e:
_logger.exception('create_english failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/ai-course/ielts/create
# ------------------------------------------------------------------
@http.route('/api/ai-course/ielts/create', type='http', auth='none',
methods=['POST'], csrf=False)
@jwt_required
def create_ielts(self, **kw):
try:
body = _get_json_body()
skill = body.get('skill')
target_band = body.get('target_band')
if not skill:
return _error_response('skill is required', 400)
if not target_band:
return _error_response('target_band is required', 400)
valid_skills = ('listening', 'reading', 'writing', 'speaking')
if skill not in valid_skills:
return _error_response(
f'skill must be one of: {", ".join(valid_skills)}', 400,
)
brief = {
'skill': skill,
'target_band': float(target_band),
'custom_instructions': body.get('brief', ''),
}
Log = request.env['encoach.ai.ielts.generation.log'].sudo()
log = Log.create({
'skill': skill,
'status': 'format_check',
'brief': json.dumps(brief),
'attempts': 1,
})
return _json_response({
'log_id': log.id,
'status': log.status,
'skill': log.skill,
}, 201)
except Exception as e:
_logger.exception('create_ielts failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/<int:course_id>/quality
# ------------------------------------------------------------------
@http.route('/api/ai-course/<int:course_id>/quality', type='http',
auth='none', methods=['GET'], csrf=False)
@jwt_required
def quality(self, course_id, **kw):
try:
Log = request.env['encoach.ai.generation.log'].sudo()
log = Log.browse(course_id)
if not log.exists():
return _error_response('Generation log not found', 404)
return _json_response({
'status': log.status,
'readability_score': 0.0,
'cefr_alignment': log.status in ('quality_check', 'approved'),
'grammar_issues': [],
'attempts': log.attempts,
})
except Exception as e:
_logger.exception('quality check failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/ai-course/<int:course_id>/approve
# ------------------------------------------------------------------
@http.route('/api/ai-course/<int:course_id>/approve', type='http',
auth='none', methods=['POST'], csrf=False)
@jwt_required
def approve(self, course_id, **kw):
try:
Log = request.env['encoach.ai.generation.log'].sudo()
log = Log.browse(course_id)
if not log.exists():
return _error_response('Generation log not found', 404)
if log.status == 'approved':
return _error_response('Already approved', 400)
log.write({
'status': 'approved',
'approved_by': request.env.user.id,
})
return _json_response({'approved': True})
except Exception as e:
_logger.exception('approve failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/ai-course/<int:course_id>/reject
# ------------------------------------------------------------------
@http.route('/api/ai-course/<int:course_id>/reject', type='http',
auth='none', methods=['POST'], csrf=False)
@jwt_required
def reject(self, course_id, **kw):
try:
body = _get_json_body()
Log = request.env['encoach.ai.generation.log'].sudo()
log = Log.browse(course_id)
if not log.exists():
return _error_response('Generation log not found', 404)
reason = body.get('reason', '')
can_retry = log.attempts < 3
if can_retry:
log.write({
'status': 'generating',
'attempts': log.attempts + 1,
'error_log': reason or log.error_log,
})
else:
log.write({
'status': 'rejected',
'error_log': reason or log.error_log,
})
return _json_response({
'rejected': True,
'can_retry': can_retry,
})
except Exception as e:
_logger.exception('reject failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/<int:course_id>/validation
# ------------------------------------------------------------------
@http.route('/api/ai-course/<int:course_id>/validation', type='http',
auth='none', methods=['GET'], csrf=False)
@jwt_required
def validation(self, course_id, **kw):
try:
IeltsLog = request.env['encoach.ai.ielts.generation.log'].sudo()
ielts_log = IeltsLog.browse(course_id)
if ielts_log.exists():
fmt_result = {}
band_result = {}
try:
fmt_result = json.loads(ielts_log.format_check_result or '{}')
except (json.JSONDecodeError, TypeError):
pass
try:
band_result = json.loads(ielts_log.band_check_result or '{}')
except (json.JSONDecodeError, TypeError):
pass
overall_passed = bool(
ielts_log.format_check_result and ielts_log.band_check_result
)
return _json_response({
'type': 'ielts',
'validation_results': {
'format_check_result': fmt_result,
'band_check_result': band_result,
},
'overall_passed': overall_passed,
})
Log = request.env['encoach.ai.generation.log'].sudo()
log = Log.browse(course_id)
if not log.exists():
return _error_response('Generation log not found', 404)
validation_results = {
'status': log.status,
'quality_passed': log.status in ('approved', 'pending_review'),
'attempts': log.attempts,
}
return _json_response({
'type': 'english',
'validation_results': validation_results,
'overall_passed': log.status == 'approved',
})
except Exception as e:
_logger.exception('validation check failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/review-queue
# ------------------------------------------------------------------
@http.route('/api/ai-course/review-queue', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def review_queue(self, **kw):
try:
body = _get_json_body() if request.httprequest.content_length else {}
page = int(kw.get('page', 1))
size = int(kw.get('size', 20))
Resource = request.env['encoach.resource'].sudo()
domain = [('ai_generated', '=', True), ('review_status', '=', 'pending')]
total = Resource.search_count(domain)
resources = Resource.search(domain, limit=size, offset=(page - 1) * size, order='create_date desc')
return _json_response({
'total': total,
'page': page,
'size': size,
'items': [r.to_api_dict() for r in resources],
})
except Exception as e:
_logger.exception('review_queue failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/ai-course/review/<int:resource_id>
# ------------------------------------------------------------------
@http.route('/api/ai-course/review/<int:resource_id>', type='http', auth='none',
methods=['POST'], csrf=False)
@jwt_required
def review_resource(self, resource_id, **kw):
try:
body = _get_json_body()
action = body.get('action')
notes = body.get('notes', '')
if action not in ('approve', 'reject'):
return _error_response('action must be approve or reject', 400)
Resource = request.env['encoach.resource'].sudo()
resource = Resource.browse(resource_id)
if not resource.exists():
return _error_response('Resource not found', 404)
vals = {
'review_status': 'approved' if action == 'approve' else 'rejected',
'approved': action == 'approve',
}
resource.write(vals)
return _json_response({'status': vals['review_status'], 'resource_id': resource_id})
except Exception as e:
_logger.exception('review_resource failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/ielts-review-queue
# ------------------------------------------------------------------
@http.route('/api/ai-course/ielts-review-queue', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def ielts_review_queue(self, **kw):
try:
page = int(kw.get('page', 1))
size = int(kw.get('size', 20))
Log = request.env['encoach.ai.ielts.generation.log'].sudo()
domain = [('review_status', '=', 'pending_review')]
total = Log.search_count(domain)
logs = Log.search(domain, limit=size, offset=(page - 1) * size, order='create_date desc')
items = []
for log in logs:
items.append({
'id': log.id,
'skill': log.skill or '',
'band_target': log.band_target if hasattr(log, 'band_target') else 0,
'review_status': log.review_status,
'created_at': log.create_date.isoformat() if log.create_date else '',
})
return _json_response({
'total': total,
'page': page,
'size': size,
'items': items,
})
except Exception as e:
_logger.exception('ielts_review_queue failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/ai-course/ielts-review/<int:log_id>
# ------------------------------------------------------------------
@http.route('/api/ai-course/ielts-review/<int:log_id>', type='http', auth='none',
methods=['POST'], csrf=False)
@jwt_required
def ielts_review(self, log_id, **kw):
try:
body = _get_json_body()
action = body.get('action')
examiner_notes = body.get('examiner_notes', '')
if action not in ('approve', 'reject', 'revise'):
return _error_response('action must be approve, reject, or revise', 400)
Log = request.env['encoach.ai.ielts.generation.log'].sudo()
log = Log.browse(log_id)
if not log.exists():
return _error_response('Log not found', 404)
status_map = {
'approve': 'approved',
'reject': 'rejected',
'revise': 'revision_needed',
}
log.write({
'review_status': status_map[action],
'examiner_id': request.env.user.id,
'examiner_notes': examiner_notes,
'reviewed_at': fields.Datetime.now(),
})
return _json_response({'status': status_map[action], 'log_id': log_id})
except Exception as e:
_logger.exception('ielts_review failed')
return _error_response(str(e), 500)

View File

@@ -0,0 +1,2 @@
from . import ai_generation_log
from . import ai_ielts_generation_log

View File

@@ -0,0 +1,25 @@
from odoo import models, fields
class EncoachAiGenerationLog(models.Model):
_name = 'encoach.ai.generation.log'
_description = 'AI Generation Log'
student_id = fields.Many2one('res.users', ondelete='set null')
course_type = fields.Selection([
('general_english', 'General English'),
('ielts', 'IELTS'),
], required=True)
brief = fields.Text()
attempts = fields.Integer(default=0)
final_resource_id = fields.Integer(string='Final Resource ID')
approved_by = fields.Many2one('res.users', ondelete='set null')
status = fields.Selection([
('generating', 'Generating'),
('quality_check', 'Quality Check'),
('pending_review', 'Pending Review'),
('approved', 'Approved'),
('rejected', 'Rejected'),
], default='generating', required=True)
created_at = fields.Datetime(default=fields.Datetime.now)
error_log = fields.Text()

View File

@@ -0,0 +1,35 @@
from odoo import models, fields
class EncoachAiIeltsGenerationLog(models.Model):
_name = 'encoach.ai.ielts.generation.log'
_description = 'AI IELTS Generation Log'
skill = fields.Selection([
('listening', 'Listening'),
('reading', 'Reading'),
('writing', 'Writing'),
('speaking', 'Speaking'),
], required=True)
brief = fields.Text()
format_check_result = fields.Text()
band_check_result = fields.Text()
examiner_id = fields.Many2one('res.users', ondelete='set null')
status = fields.Selection([
('generating', 'Generating'),
('format_check', 'Format Check'),
('examiner_review', 'Examiner Review'),
('approved', 'Approved'),
('rejected', 'Rejected'),
], default='generating', required=True)
attempts = fields.Integer(default=0)
created_at = fields.Datetime(default=fields.Datetime.now)
review_status = fields.Selection([
('draft', 'Draft'),
('pending_review', 'Pending Review'),
('approved', 'Approved'),
('rejected', 'Rejected'),
('revision_needed', 'Revision Needed'),
], default='draft')
examiner_notes = fields.Text()
reviewed_at = fields.Datetime()

View File

@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_encoach_ai_generation_log_user,encoach.ai.generation.log.user,model_encoach_ai_generation_log,base.group_user,1,1,1,1
access_encoach_ai_ielts_generation_log_user,encoach.ai.ielts.generation.log.user,model_encoach_ai_ielts_generation_log,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_encoach_ai_generation_log_user encoach.ai.generation.log.user model_encoach_ai_generation_log base.group_user 1 1 1 1
3 access_encoach_ai_ielts_generation_log_user encoach.ai.ielts.generation.log.user model_encoach_ai_ielts_generation_log base.group_user 1 1 1 1

View File

@@ -0,0 +1,2 @@
from .english_pipeline import EnglishPipeline
from .ielts_pipeline import IeltsPipeline

View File

@@ -0,0 +1,124 @@
import json
import logging
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
except ImportError:
OpenAIService = None
try:
from odoo.addons.encoach_quality_gate.services.content_gate import ContentSourceGate
except ImportError:
ContentSourceGate = None
_logger = logging.getLogger(__name__)
class EnglishPipeline:
"""AI content generation pipeline for General English courses."""
def __init__(self, env):
self.env = env
self.ai = OpenAIService(env)
def generate_content(self, env, gap_profile, cefr_level):
"""Generate General English content based on gap analysis.
Args:
env: Odoo environment.
gap_profile: dict with 'skill_gaps' list describing weak areas.
cefr_level: target CEFR level string (e.g. 'B1').
Returns:
dict with generated content.
"""
skill_gaps = gap_profile.get('skill_gaps', [])
gaps_description = ', '.join(skill_gaps) if skill_gaps else 'general skills'
messages = [
{'role': 'system', 'content': (
'You are an expert English language curriculum designer. '
'Return JSON with keys: "title", "objectives", "units" '
'(each unit has "topic", "grammar_focus", "vocabulary", '
'"reading_text", "exercises").'
)},
{'role': 'user', 'content': (
f'Generate a General English course at CEFR {cefr_level} level. '
f'Focus on these gap areas: {gaps_description}. '
f'Include practical, real-world content appropriate for the level.'
)},
]
try:
content = self.ai.chat_json(messages, temperature=0.8)
except Exception as e:
_logger.error("English content generation failed: %s", e)
env['encoach.ai.generation.log'].create({
'course_type': 'general_english',
'brief': json.dumps(gap_profile),
'status': 'rejected',
'error_log': str(e),
})
return {'error': str(e)}
resource = env['encoach.ai.generation.log'].create({
'course_type': 'general_english',
'brief': json.dumps(gap_profile),
'status': 'quality_check',
'attempts': 1,
})
# Apply content source gate logic
if ContentSourceGate is not None:
try:
ContentSourceGate.apply_gate(resource)
except Exception as e:
_logger.warning("ContentSourceGate.apply_gate failed: %s", e)
return content
def quality_check(self, env, content, cefr_level):
"""Run quality gate checks on generated content.
Args:
env: Odoo environment.
content: dict of generated content to validate.
cefr_level: target CEFR level for alignment check.
Returns:
dict with 'passed' bool and 'issues' list.
"""
issues = []
if not content.get('units'):
issues.append('No units generated')
else:
for i, unit in enumerate(content['units'], 1):
if not unit.get('exercises'):
issues.append(f'Unit {i} has no exercises')
if not unit.get('reading_text'):
issues.append(f'Unit {i} has no reading text')
if not content.get('objectives'):
issues.append('No learning objectives defined')
messages = [
{'role': 'system', 'content': (
'You are a CEFR alignment expert. Return JSON with keys: '
'"aligned" (bool) and "issues" (list of strings).'
)},
{'role': 'user', 'content': (
f'Check if this content is aligned with CEFR {cefr_level}: '
f'{json.dumps(content)}'
)},
]
try:
alignment = self.ai.chat_json(messages, temperature=0.3)
if not alignment.get('aligned'):
issues.extend(alignment.get('issues', ['CEFR misalignment detected']))
except Exception as e:
_logger.warning("CEFR alignment check failed: %s", e)
issues.append(f'CEFR alignment check error: {e}')
return {'passed': len(issues) == 0, 'issues': issues}

View File

@@ -0,0 +1,170 @@
import json
import logging
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
except ImportError:
OpenAIService = None
try:
from odoo.addons.encoach_quality_gate.services.content_gate import ContentSourceGate
except ImportError:
ContentSourceGate = None
_logger = logging.getLogger(__name__)
SKILL_PROMPTS = {
'listening': (
'Generate an IELTS Listening section. Return JSON with keys: '
'"script", "speakers", "duration_estimate", "questions".'
),
'reading': (
'Generate an IELTS Reading passage with questions. Return JSON with keys: '
'"passage", "word_count", "questions", "question_types".'
),
'writing': (
'Generate an IELTS Writing task. Return JSON with keys: '
'"task_type", "prompt", "requirements", "sample_band_descriptors".'
),
'speaking': (
'Generate an IELTS Speaking part. Return JSON with keys: '
'"part", "questions", "cue_card" (if part 2), "follow_up_questions".'
),
}
IELTS_FORMAT_RULES = {
'listening': {'required_keys': ['script', 'questions'], 'min_questions': 10},
'reading': {'required_keys': ['passage', 'questions'], 'min_word_count': 500},
'writing': {'required_keys': ['prompt', 'requirements']},
'speaking': {'required_keys': ['questions']},
}
class IeltsPipeline:
"""AI content generation pipeline for IELTS skill-specific content."""
def __init__(self, env):
self.env = env
self.ai = OpenAIService(env)
def generate_content(self, env, skill, brief):
"""Generate IELTS skill-specific content.
Args:
env: Odoo environment.
skill: one of 'listening', 'reading', 'writing', 'speaking'.
brief: dict with generation parameters.
Returns:
dict with generated content.
"""
system_prompt = SKILL_PROMPTS.get(skill, SKILL_PROMPTS['reading'])
brief_text = json.dumps(brief) if isinstance(brief, dict) else str(brief)
messages = [
{'role': 'system', 'content': (
f'You are an expert IELTS content creator. {system_prompt}'
)},
{'role': 'user', 'content': (
f'Generate IELTS {skill} content based on this brief: {brief_text}'
)},
]
try:
content = self.ai.chat_json(messages, temperature=0.8)
except Exception as e:
_logger.error("IELTS %s content generation failed: %s", skill, e)
return {'error': str(e)}
resource = env['encoach.ai.ielts.generation.log'].create({
'skill': skill,
'brief': brief_text,
'status': 'format_check',
'attempts': 1,
})
# Apply content source gate logic
if ContentSourceGate is not None:
try:
ContentSourceGate.apply_gate(resource)
except Exception as e:
_logger.warning("ContentSourceGate.apply_gate failed: %s", e)
return content
def format_check(self, env, content, skill):
"""Validate IELTS format compliance for a given skill.
Args:
env: Odoo environment.
content: dict of generated content.
skill: IELTS skill type.
Returns:
dict with 'passed' bool and 'errors' list.
"""
errors = []
rules = IELTS_FORMAT_RULES.get(skill, {})
for key in rules.get('required_keys', []):
if key not in content or not content[key]:
errors.append(f'Missing required key: {key}')
if skill == 'listening':
questions = content.get('questions', [])
min_q = rules.get('min_questions', 10)
if len(questions) < min_q:
errors.append(
f'Listening requires at least {min_q} questions, '
f'got {len(questions)}'
)
if skill == 'reading':
passage = content.get('passage', '')
min_wc = rules.get('min_word_count', 500)
word_count = len(passage.split()) if passage else 0
if word_count < min_wc:
errors.append(
f'Reading passage must be at least {min_wc} words, '
f'got {word_count}'
)
if skill == 'speaking' and not content.get('questions'):
errors.append('Speaking section requires at least one question')
return {'passed': len(errors) == 0, 'errors': errors}
def band_calibration(self, env, content, target_band):
"""Check content aligns with target IELTS band level.
Args:
env: Odoo environment.
content: dict of generated content.
target_band: float target band score (e.g. 6.5).
Returns:
dict with 'passed' bool and 'issues' list.
"""
messages = [
{'role': 'system', 'content': (
'You are an IELTS band calibration expert. Evaluate whether '
'the provided content matches the target band level. '
'Return JSON with keys: "calibrated" (bool), "estimated_band" (float), '
'"issues" (list of strings).'
)},
{'role': 'user', 'content': (
f'Target band: {target_band}. '
f'Content: {json.dumps(content)}'
)},
]
try:
result = self.ai.chat_json(messages, temperature=0.3)
except Exception as e:
_logger.error("Band calibration failed: %s", e)
return {'passed': False, 'issues': [f'Band calibration error: {e}']}
return {
'passed': result.get('calibrated', False),
'issues': result.get('issues', []),
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_ai_generation_logs"
name="AI Generation Logs"
parent="encoach_core.menu_encoach_courses"
action="encoach_ai_course.action_ai_generation_log"
sequence="30"/>
<menuitem id="menu_ai_ielts_logs"
name="IELTS Generation Logs"
parent="encoach_core.menu_encoach_courses"
action="encoach_ai_course.action_ai_ielts_log"
sequence="40"/>
</odoo>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_ai_generation_log_form" model="ir.ui.view">
<field name="name">encoach.ai.generation.log.form</field>
<field name="model">encoach.ai.generation.log</field>
<field name="arch" type="xml">
<form string="AI Generation Log">
<header>
<field name="status" widget="statusbar" statusbar_visible="generating,quality_check,pending_review,approved"/>
</header>
<sheet>
<group>
<group>
<field name="student_id"/>
<field name="course_type"/>
<field name="attempts"/>
</group>
<group>
<field name="final_resource_id"/>
<field name="approved_by"/>
<field name="created_at"/>
</group>
</group>
<notebook>
<page string="Brief" name="brief">
<field name="brief"/>
</page>
<page string="Error Log" name="error_log">
<field name="error_log"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_ai_generation_log_list" model="ir.ui.view">
<field name="name">encoach.ai.generation.log.list</field>
<field name="model">encoach.ai.generation.log</field>
<field name="arch" type="xml">
<list string="AI Generation Logs">
<field name="course_type"/>
<field name="status" widget="badge" decoration-info="status == 'generating'" decoration-warning="status == 'pending_review'" decoration-success="status == 'approved'" decoration-danger="status == 'rejected'"/>
<field name="attempts"/>
<field name="student_id"/>
<field name="created_at"/>
</list>
</field>
</record>
<record id="view_ai_generation_log_search" model="ir.ui.view">
<field name="name">encoach.ai.generation.log.search</field>
<field name="model">encoach.ai.generation.log</field>
<field name="arch" type="xml">
<search string="Search AI Generation Logs">
<field name="student_id"/>
<separator/>
<filter string="Generating" name="generating" domain="[('status', '=', 'generating')]"/>
<filter string="Quality Check" name="quality_check" domain="[('status', '=', 'quality_check')]"/>
<filter string="Pending Review" name="pending_review" domain="[('status', '=', 'pending_review')]"/>
<filter string="Approved" name="approved" domain="[('status', '=', 'approved')]"/>
<filter string="Rejected" name="rejected" domain="[('status', '=', 'rejected')]"/>
<separator/>
<filter string="General English" name="general_english" domain="[('course_type', '=', 'general_english')]"/>
<filter string="IELTS" name="ielts" domain="[('course_type', '=', 'ielts')]"/>
</search>
</field>
</record>
<record id="action_ai_generation_log" model="ir.actions.act_window">
<field name="name">AI Generation Logs</field>
<field name="res_model">encoach.ai.generation.log</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_ai_ielts_log_form" model="ir.ui.view">
<field name="name">encoach.ai.ielts.generation.log.form</field>
<field name="model">encoach.ai.ielts.generation.log</field>
<field name="arch" type="xml">
<form string="IELTS Generation Log">
<header>
<field name="status" widget="statusbar" statusbar_visible="generating,format_check,examiner_review,approved"/>
</header>
<sheet>
<group>
<group>
<field name="skill"/>
<field name="attempts"/>
<field name="examiner_id"/>
</group>
<group>
<field name="created_at"/>
</group>
</group>
<notebook>
<page string="Brief" name="brief">
<field name="brief"/>
</page>
<page string="Format Check Result" name="format_check">
<field name="format_check_result"/>
</page>
<page string="Band Check Result" name="band_check">
<field name="band_check_result"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_ai_ielts_log_list" model="ir.ui.view">
<field name="name">encoach.ai.ielts.generation.log.list</field>
<field name="model">encoach.ai.ielts.generation.log</field>
<field name="arch" type="xml">
<list string="IELTS Generation Logs">
<field name="skill"/>
<field name="status" widget="badge" decoration-info="status == 'generating'" decoration-warning="status == 'examiner_review'" decoration-success="status == 'approved'" decoration-danger="status == 'rejected'"/>
<field name="attempts"/>
<field name="examiner_id"/>
<field name="created_at"/>
</list>
</field>
</record>
<record id="action_ai_ielts_log" model="ir.actions.act_window">
<field name="name">IELTS Generation Logs</field>
<field name="res_model">encoach.ai.ielts.generation.log</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_ai_review_queue_list" model="ir.ui.view">
<field name="name">encoach.resource.ai.review.list</field>
<field name="model">encoach.resource</field>
<field name="arch" type="xml">
<list string="AI Content Review Queue">
<field name="name"/>
<field name="type"/>
<field name="cefr_level"/>
<field name="grammar_topic"/>
<field name="review_status" widget="badge" decoration-info="review_status == 'pending'" decoration-success="review_status == 'approved'" decoration-danger="review_status == 'rejected'"/>
<field name="create_date"/>
</list>
</field>
</record>
<record id="action_ai_review_queue" model="ir.actions.act_window">
<field name="name">AI Content Review</field>
<field name="res_model">encoach.resource</field>
<field name="view_mode">list,form</field>
<field name="domain">[('ai_generated', '=', True)]</field>
<field name="context">{'search_default_pending_review': 1}</field>
</record>
</odoo>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_ielts_review_form" model="ir.ui.view">
<field name="name">encoach.ai.ielts.generation.log.review.form</field>
<field name="model">encoach.ai.ielts.generation.log</field>
<field name="arch" type="xml">
<form string="IELTS Content Review">
<header>
<field name="review_status" widget="statusbar" statusbar_visible="draft,pending_review,approved"/>
</header>
<sheet>
<group>
<group>
<field name="skill" readonly="1"/>
<field name="review_status"/>
</group>
<group>
<field name="examiner_id"/>
<field name="reviewed_at"/>
</group>
</group>
<group string="Examiner Notes">
<field name="examiner_notes" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_ielts_review_list" model="ir.ui.view">
<field name="name">encoach.ai.ielts.generation.log.review.list</field>
<field name="model">encoach.ai.ielts.generation.log</field>
<field name="arch" type="xml">
<list string="IELTS Content Review Queue">
<field name="skill"/>
<field name="review_status" widget="badge" decoration-info="review_status == 'pending_review'" decoration-success="review_status == 'approved'" decoration-danger="review_status == 'rejected'" decoration-warning="review_status == 'revision_needed'"/>
<field name="examiner_id"/>
<field name="reviewed_at"/>
<field name="create_date"/>
</list>
</field>
</record>
<record id="action_ielts_review_queue" model="ir.actions.act_window">
<field name="name">IELTS Content Review</field>
<field name="res_model">encoach.ai.ielts.generation.log</field>
<field name="view_mode">list,form</field>
<field name="domain">[('review_status', '\!=', 'draft')]</field>
</record>
</odoo>