diff --git a/frontend/src/app/shared/lesson-viewer.component.scss b/frontend/src/app/shared/lesson-viewer.component.scss
index 6ab1451..3371346 100644
--- a/frontend/src/app/shared/lesson-viewer.component.scss
+++ b/frontend/src/app/shared/lesson-viewer.component.scss
@@ -7,6 +7,22 @@
min-width: 0;
}
+.lesson-viewer__step-instruction {
+ color: var(--ee-text-soft, #475569);
+ font-size: 0.9rem;
+ line-height: 1.55;
+}
+
+.lesson-viewer__activity-error {
+ background: var(--ee-color-status-danger-background, #fff1f3);
+ border: 1px solid var(--ee-color-status-danger-border, #ffd9df);
+ border-radius: var(--ee-radius-card-default, 0.5rem);
+ color: var(--ee-color-status-danger-text, #9f1239);
+ font-size: 0.9rem;
+ line-height: 1.5;
+ padding: 0.75rem;
+}
+
.lesson-viewer__step-list {
display: flex;
gap: 0.5rem;
diff --git a/frontend/src/app/shared/lesson-viewer.component.spec.ts b/frontend/src/app/shared/lesson-viewer.component.spec.ts
index 48a0eef..948a4ce 100644
--- a/frontend/src/app/shared/lesson-viewer.component.spec.ts
+++ b/frontend/src/app/shared/lesson-viewer.component.spec.ts
@@ -102,6 +102,40 @@ describe('LessonViewerComponent', () => {
expect(radiogroup.getAttribute('aria-label')).toBe('Quiz response options');
});
+ it('keeps quiz activity input in place and announces validation before advancing', () => {
+ component.lesson = {
+ ...component.lesson,
+ hook: undefined,
+ content: undefined,
+ guided_practice: undefined,
+ discussion_questions: [],
+ activities: [{
+ title: 'Check Your Understanding',
+ type: 'quiz',
+ content: JSON.stringify({
+ question: 'Which claim is best supported?',
+ options: ['Claim A', 'Claim B'],
+ }),
+ } as any],
+ };
+
+ fixture.detectChanges();
+ component.goToStep(component.lessonSteps.findIndex(step => step.kind === 'activity'));
+ fixture.detectChanges();
+
+ component.goToNextStep();
+ fixture.detectChanges();
+
+ expect(component.currentStep?.kind).toBe('activity');
+ expect(fixture.nativeElement.textContent).toContain('Choose an answer before continuing');
+
+ component.selectedOption = 'Claim A';
+ fixture.detectChanges();
+ component.goToNextStep();
+
+ expect(component.activityErrorMessage).toBe('');
+ });
+
it('preserves next-activity flow for richer seeded activity patterns', () => {
component.lesson = {
...component.lesson,
diff --git a/frontend/src/app/shared/lesson-viewer.component.ts b/frontend/src/app/shared/lesson-viewer.component.ts
index 4b99b57..2337475 100644
--- a/frontend/src/app/shared/lesson-viewer.component.ts
+++ b/frontend/src/app/shared/lesson-viewer.component.ts
@@ -32,6 +32,7 @@ export class LessonViewerComponent implements OnChanges {
currentStepIndex = 0;
reflectionResponse = '';
selectedOption = '';
+ activityErrorMessage = '';
ngOnChanges(changes: SimpleChanges): void {
if (changes['lesson']) {
@@ -39,6 +40,7 @@ export class LessonViewerComponent implements OnChanges {
this.currentStepIndex = 0;
this.reflectionResponse = '';
this.selectedOption = '';
+ this.activityErrorMessage = '';
}
}
@@ -138,6 +140,36 @@ export class LessonViewerComponent implements OnChanges {
return this.currentStep?.label || 'Lesson';
}
+ get currentStepStateLabel(): string {
+ if (!this.currentStep) {
+ return 'Not started';
+ }
+ if (this.isLastActivity) {
+ return 'Ready to complete';
+ }
+ if (this.currentStep.kind === 'activity' && this.quizPayload && !this.selectedOption) {
+ return 'In progress';
+ }
+ return 'Ready to continue';
+ }
+
+ get currentStepInstruction(): string {
+ const step = this.currentStep;
+ if (!step) {
+ return 'Start the lesson when you are ready.';
+ }
+ if (step.kind === 'activity' && this.quizPayload) {
+ return 'Choose one answer before moving to the next step.';
+ }
+ if (step.kind === 'activity' && this.currentActivity?.type === 'reflection') {
+ return 'Write your reflection, then continue when you are ready.';
+ }
+ if (step.kind === 'discussion') {
+ return 'Use the prompts to think, talk, draw, or write.';
+ }
+ return 'Review this step, then continue when you are ready.';
+ }
+
get currentStepTitle(): string {
const step = this.currentStep;
if (!step) {
@@ -185,6 +217,9 @@ export class LessonViewerComponent implements OnChanges {
}
goToNextStep(): void {
+ if (!this.canLeaveCurrentStep()) {
+ return;
+ }
if (this.currentStepIndex < this.stepCount - 1) {
this.currentStepIndex++;
this.syncActivityIndex();
@@ -209,6 +244,16 @@ export class LessonViewerComponent implements OnChanges {
private resetStepInputs(): void {
this.reflectionResponse = '';
this.selectedOption = '';
+ this.activityErrorMessage = '';
+ }
+
+ private canLeaveCurrentStep(): boolean {
+ if (this.currentStep?.kind === 'activity' && this.quizPayload && !this.selectedOption) {
+ this.activityErrorMessage = 'Choose an answer before continuing. Your selection is not submitted until the lesson is completed.';
+ return false;
+ }
+ this.activityErrorMessage = '';
+ return true;
}
isYouTubeLink(url: string): boolean {
diff --git a/frontend/tests/demo/student-flagship-smoke.spec.ts b/frontend/tests/demo/student-flagship-smoke.spec.ts
index e84fc9c..7d72535 100644
--- a/frontend/tests/demo/student-flagship-smoke.spec.ts
+++ b/frontend/tests/demo/student-flagship-smoke.spec.ts
@@ -11,13 +11,13 @@ test.describe('demo student flagship smoke', () => {
await loginAsDemoStudent(page);
await expectFlagshipCourseOnStudentDashboard(page);
- await expect(page.getByText('Products organize your access')).toBeVisible();
- await page.getByRole('button', { name: /continue/i }).first().click();
+ await expect(page.getByText('Next learning')).toBeVisible();
+ await page.getByRole('button', { name: /start lesson|resume lesson/i }).first().click();
await expect(page).toHaveURL(/\/learn\/lesson\//);
await expect(page.getByLabel('Lesson experience')).toBeVisible();
await expect(
- page.getByRole('button', { name: 'Exit lesson and return to dashboard' }),
+ page.getByRole('button', { name: 'Exit lesson and return to Learn' }),
).toBeVisible();
});
});
diff --git a/frontend/tests/demo/support/demo-smoke.ts b/frontend/tests/demo/support/demo-smoke.ts
index 148c091..a095d06 100644
--- a/frontend/tests/demo/support/demo-smoke.ts
+++ b/frontend/tests/demo/support/demo-smoke.ts
@@ -16,7 +16,7 @@ export async function loginAsDemoStudent(page: Page): Promise {
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/learn$/);
- await expect(page.getByRole('heading', { name: 'Your products and learning paths' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Continue your learning' })).toBeVisible();
}
export async function expectFlagshipCourseOnStudentDashboard(page: Page): Promise {
diff --git a/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md b/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md
index 61e46f0..66840e1 100644
--- a/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md
+++ b/openspec/changes/overhaul-role-based-ui-ux-experience/tasks.md
@@ -26,10 +26,16 @@
## 5. Student Experience
-- [ ] 5.1 Make `/learn` the student-first dashboard using existing learner/student data; affected screens: `/learn`, current student dashboard; APIs: `/api/student-courses`, `/api/analytics/student-progress`, badge/certification/program APIs; tests: student view specs; a11y: next action first; responsive: mobile bottom nav; frontend-only: yes; rollback: current dashboard.
-- [ ] 5.2 Redesign learner course library and course overview; affected screens: `/learn/products`, `/home/courses`; APIs: `/api/courses`, `/api/learner-portal/products`, `/api/enroll`; tests: service/page specs; a11y: card labels/status; responsive: filter drawer; frontend-only: yes; rollback: current catalog.
-- [ ] 5.3 Redesign lesson player and activity/assessment states without changing governed progress APIs; affected screens: `/learn/lesson/:id`, `/home/lesson/:id`, `/home/assessments/:id`; APIs: progress, lessons, assessments; tests: lesson/assessment specs and Playwright student smoke; a11y: live save/advance announcements; responsive: sticky mobile action bar; frontend-only: yes; rollback: old lesson template.
-- [ ] 5.4 Redesign progress, achievements, certifications, and learner resources; affected screens: progress sections, `/learn/certificates`, `/learn/resources`; APIs: analytics, badges, certifications, resources; tests: component/page specs; a11y: no color-only progress; responsive: single-column mobile; frontend-only: yes; rollback: previous sections.
+- [x] 5.1 Make `/learn` the student-first dashboard using existing learner/student data; affected screens: `/learn`, current student dashboard; APIs: `/api/student-courses`, `/api/analytics/student-progress`, badge/certification/program APIs; tests: student view specs; a11y: next action first; responsive: mobile bottom nav; frontend-only: yes; rollback: current dashboard.
+- [x] 5.2 Redesign learner course library and course overview; affected screens: `/learn/products`, `/home/courses`; APIs: `/api/courses`, `/api/learner-portal/products`, `/api/enroll`; tests: service/page specs; a11y: card labels/status; responsive: filter drawer; frontend-only: yes; rollback: current catalog.
+ - [x] 5.2a Reframe `/learn/products` as student Courses/available learning while preserving the existing learner-product API.
+ - [x] 5.2b Add canonical `/learn/courses/:courseId` overview with reusable unit/lesson hierarchy and governed start/resume actions.
+- [x] 5.3 Redesign lesson player and activity/assessment states without changing governed progress APIs; affected screens: `/learn/lesson/:id`, `/home/lesson/:id`, `/home/assessments/:id`; APIs: progress, lessons, assessments; tests: lesson/assessment specs and Playwright student smoke; a11y: live save/advance announcements; responsive: sticky mobile action bar; frontend-only: yes; rollback: old lesson template.
+ - [x] 5.3a Add canonical `/learn` return behavior plus completion saving and failure states to the governed lesson runtime.
+ - [x] 5.3b Add course/unit context to lesson runtime, local activity validation states, and assessment final confirmation/result states.
+- [x] 5.4 Redesign progress, achievements, certifications, and learner resources; affected screens: progress sections, `/learn/certificates`, `/learn/resources`; APIs: analytics, badges, certifications, resources; tests: component/page specs; a11y: no color-only progress; responsive: single-column mobile; frontend-only: yes; rollback: previous sections.
+ - [x] 5.4a Add `/learn` progress summary plus badge/certification preview and honest resources empty state.
+ - [x] 5.4b Refine `/learn/certificates` as a student achievements/certificates view with shared loading, empty, and error states.
## 6. Teacher Experience