Backend (encoach_lms_api):
- New models: encoach.exam.security.event, encoach.live.session,
encoach.live.session.participant, encoach.handwritten.submission.
- Extended: encoach.exam.custom (security_level, single_attempt,
suspicious_event_threshold), encoach.course.plan (is_personalized,
personalized_for_id, weakness_summary), encoach.entity (turnitin_*).
- New controllers / 24 endpoints:
* exam_security: POST /api/exam/security/event,
GET /api/exam/single-attempt-check,
GET /api/teacher/exam/<id>/security/events,
GET /api/teacher/exam/attempt/<id>/security/events,
GET /api/teacher/exam/<id>/live.
* live_sessions: full CRUD + /notify, /join, /leave, /end,
/recording, /remove-participant, /attendance, /ics
(RFC-5545 iCalendar with VALARM + ATTENDEE/ORGANIZER, attached to
invite emails so Gmail/Outlook/Apple Mail prompt to add).
* personalized_plan: GET/POST /api/student/personalized-plan
(CEFR / weeks / focus / grammar_focus payloads).
* handwritten: student upload + AI grading (pytesseract OCR +
OpenAI scoring) + teacher review override.
* reports_export: CSV + PDF export for student-performance and
record reports.
* turnitin: per-entity settings (admin), test connection, submit
text. Real upload stubbed pending institutional API key.
- Manifest: depends now lists encoach_scoring, encoach_exam_template,
encoach_ai_course so security/live-session models load cleanly.
- teacher_insights.py: fixed assignment traversal via op.batch.course_id,
res.users mapping, and capped rate_completed at 100% (unique
student/exam pairs).
- reports.py: branch_id / subject_id / gender filters wired into the
Reports stack.
Frontend:
- New services: examSecurity, liveSession, personalizedPlan,
handwritten, turnitin, reportsExport.
- New pages: LiveSessionsPage, LiveSessionRoom (Jitsi IframeAPI embed
with host bar — Mute all / Cams off / per-row Ask-to-unmute /
Remove + log audit reason, kickParticipant after backend audit POST),
TurnitinSettings (admin), TeacherTestSessionConsole, AddToCalendar
per-card .ics button.
- New components: PersonalizedPlanCard (Quick generate + Customize
dialog: CEFR, weeks, focus, 9-checkbox grammar), AITutorAvatar,
HandwrittenSubmissionPanel, ImageLightbox, InfographicBlock
(six-tone callout with key-based auto-detect — did_you_know,
key_takeaway, tip, objective, common_mistakes, etc.), ExportButtons.
- useExamSecurity hook: tab/blur, copy/paste/cut, fullscreen exit,
suspicious shortcuts; debounced auto-post + auto-lock callback.
- StudentCourseDetail / TeacherCourseInsights: discussion now
embedded as a per-course tab.
- StudentCourses: unified My Courses + My Course Plan with
is_mandatory color-coding + Accredited Core / Supplementary
sections (always rendered, with empty states).
- MaterialBookView: special-cases image keys into ImageLightbox,
routes infographic-style keys into colored InfographicBlock cards,
splits long strings into paragraphs, rounded-2xl shadow-sm wrapper.
- MaterialViewer: ImageViewer now uses lightbox, ArticleViewer is
now a real <article class="prose">.
- Layout shells: Live Sessions in Student/Teacher sidebars, Turnitin
in Admin sidebar; EN + AR i18n keys.
- App.tsx: lazy routes for /live, /live/:id,
/teacher/exam/:assignmentId/console, /admin/turnitin.
- Visual identity refresh: warm cream bg, white sidebar with charcoal
active pills, dark CTA buttons, bright orange accents,
rounded-2xl cards.
Verification:
- All 12 new endpoints smoke-tested via curl (200 + sane payloads).
- /api/live-sessions/<id>/ics returns valid 23-line VCALENDAR.
- Frontend tsc --noEmit clean.
- Odoo restarted successfully (registry loaded in 0.77s, no errors).
- docs/PROJECT_SUMMARY.md updated with both today's polish + the
full Phase 2/3/4 rollout for the record.
Status: 27/29 institutional requirements fully wired. Turnitin
upload + LMS-side breakout/poll persistence remain partial (see
PROJECT_SUMMARY for details).
Made-with: Cursor
106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
import {
|
|
Lightbulb,
|
|
Target,
|
|
CheckCircle2,
|
|
AlertTriangle,
|
|
Info,
|
|
Star,
|
|
Sparkles,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
|
|
type Tone = "tip" | "info" | "warn" | "success" | "highlight" | "example";
|
|
|
|
const TONE_STYLE: Record<Tone, { wrapper: string; icon: LucideIcon; iconClass: string; title: string }> = {
|
|
tip: { wrapper: "border-amber-200 bg-amber-50", icon: Lightbulb, iconClass: "text-amber-600", title: "text-amber-900" },
|
|
info: { wrapper: "border-blue-200 bg-blue-50", icon: Info, iconClass: "text-blue-600", title: "text-blue-900" },
|
|
warn: { wrapper: "border-rose-200 bg-rose-50", icon: AlertTriangle, iconClass: "text-rose-600", title: "text-rose-900" },
|
|
success: { wrapper: "border-emerald-200 bg-emerald-50", icon: CheckCircle2, iconClass: "text-emerald-600", title: "text-emerald-900" },
|
|
highlight: { wrapper: "border-purple-200 bg-purple-50", icon: Sparkles, iconClass: "text-purple-600", title: "text-purple-900" },
|
|
example: { wrapper: "border-slate-200 bg-slate-50", icon: Star, iconClass: "text-slate-600", title: "text-slate-900" },
|
|
};
|
|
|
|
/**
|
|
* Visual callout block used inside lessons. The AI course generator now
|
|
* emits keys like `did_you_know`, `key_takeaway`, `tip`, `warning`,
|
|
* `objective`, `example`, etc. Instead of dumping them as `<h5>label</h5>
|
|
* <p>text</p>` we render a coloured card with an icon — that's the
|
|
* "infographic" treatment the design refresh asked for.
|
|
*/
|
|
export default function InfographicBlock({
|
|
title,
|
|
tone = "info",
|
|
children,
|
|
items,
|
|
}: {
|
|
title: string;
|
|
tone?: Tone;
|
|
children?: React.ReactNode;
|
|
items?: (string | { label?: string; value?: string })[];
|
|
}) {
|
|
const style = TONE_STYLE[tone];
|
|
const Icon = style.icon;
|
|
return (
|
|
<div className={`rounded-xl border ${style.wrapper} p-4 shadow-sm space-y-2`}>
|
|
<div className="flex items-center gap-2">
|
|
<span className="rounded-full bg-white/80 p-1.5 shadow-sm">
|
|
<Icon className={`h-4 w-4 ${style.iconClass}`} />
|
|
</span>
|
|
<div className={`text-sm font-semibold ${style.title}`}>{title}</div>
|
|
</div>
|
|
{children && <div className="text-sm leading-7 text-foreground/90">{children}</div>}
|
|
{items && items.length > 0 && (
|
|
<ul className="space-y-1.5">
|
|
{items.map((it, i) => {
|
|
if (typeof it === "string") {
|
|
return (
|
|
<li key={i} className="flex items-start gap-2 text-sm">
|
|
<Target className="h-3.5 w-3.5 mt-1 text-muted-foreground shrink-0" />
|
|
<span>{it}</span>
|
|
</li>
|
|
);
|
|
}
|
|
return (
|
|
<li key={i} className="flex items-start gap-2 text-sm">
|
|
<Target className="h-3.5 w-3.5 mt-1 text-muted-foreground shrink-0" />
|
|
<span>
|
|
{it.label && <strong className="me-1">{it.label}:</strong>}
|
|
{it.value}
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const TONE_BY_KEY: Record<string, Tone> = {
|
|
tip: "tip",
|
|
hint: "tip",
|
|
did_you_know: "tip",
|
|
fun_fact: "tip",
|
|
key_takeaway: "highlight",
|
|
takeaway: "highlight",
|
|
summary: "highlight",
|
|
highlight: "highlight",
|
|
objective: "info",
|
|
objectives: "info",
|
|
goal: "info",
|
|
goals: "info",
|
|
warning: "warn",
|
|
pitfall: "warn",
|
|
caution: "warn",
|
|
common_mistakes: "warn",
|
|
success_criteria: "success",
|
|
remember: "success",
|
|
example: "example",
|
|
examples: "example",
|
|
};
|
|
|
|
export function inferTone(key: string): Tone | null {
|
|
const norm = key.toLowerCase().replace(/[\s-]/g, "_");
|
|
return TONE_BY_KEY[norm] ?? null;
|
|
}
|