diff --git a/.gitignore b/.gitignore index 91c1447..a972c7e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts .env.e2e + +test-results/ +playwright-report/ +playwright/.cache/ diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts index 131e2ab..a71f417 100644 --- a/e2e/dashboard.spec.ts +++ b/e2e/dashboard.spec.ts @@ -31,19 +31,26 @@ const BASE_URL = 'http://localhost:3000'; // ─── Helper: Login via UI ───────────────────────────────── async function loginViaUI(page: Page) { - await page.goto(`${BASE_URL}/login`); - await page.waitForLoadState('networkidle'); + // Clear any existing session so login page doesn't redirect away + await page.goto(`${BASE_URL}/login`, { waitUntil: 'load' }); + await page.evaluate(() => { + localStorage.removeItem('ozymorlab_token'); + localStorage.removeItem('ozymorlab_refresh_token'); + }); + await page.reload({ waitUntil: 'load' }); + + // Client-side hydration after load + await page.waitForTimeout(500); + await page.waitForSelector('#login-email', { timeout: 10000 }); await page.fill('#login-email', TEST_EMAIL); await page.fill('#login-password', TEST_PASSWORD); await page.click('#login-submit'); - // Wait for either the success message or direct redirect - // The login flow is: Supabase auth → backend /auth/me → 800ms delay → router.push('/dashboard') + // Login flow: POST /auth/login → 800ms delay → router.push('/dashboard') try { await page.waitForURL('**/dashboard**', { timeout: 30000 }); } catch { - // If URL didn't change, check if there's an error displayed const errorEl = page.locator('.auth-error'); if (await errorEl.isVisible()) { const errorText = await errorEl.textContent(); @@ -59,15 +66,14 @@ async function loginViaUI(page: Page) { test.describe('Landing Page', () => { test('should render branding and CTA links', async ({ page }) => { await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('a[href="/login"]')).toBeVisible(); - await expect(page.locator('a[href="/login?tab=signup"]').first()).toBeVisible(); }); test('Sign In link navigates to login page', async ({ page }) => { await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await page.locator('a[href="/login"]').click(); await page.waitForURL('**/login**'); @@ -81,17 +87,17 @@ test.describe('Landing Page', () => { test.describe('Login Page UI', () => { test('should render login form fields', async ({ page }) => { await page.goto(`${BASE_URL}/login`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('#login-email')).toBeVisible(); await expect(page.locator('#login-password')).toBeVisible(); await expect(page.locator('#login-submit')).toBeVisible(); - await expect(page.locator('text=Edexia AIOS')).toBeVisible(); + await expect(page.locator('text=OzymorLab AIOS')).toBeVisible(); }); test('should toggle between Sign In and Create Account', async ({ page }) => { await page.goto(`${BASE_URL}/login`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await page.click('button:has-text("Create Account")'); await expect(page.locator('#signup-name')).toBeVisible(); @@ -103,7 +109,7 @@ test.describe('Login Page UI', () => { test('should toggle password visibility', async ({ page }) => { await page.goto(`${BASE_URL}/login`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await page.fill('#login-password', 'test'); await page.locator('.form-input-toggle').click(); @@ -112,7 +118,7 @@ test.describe('Login Page UI', () => { test('should validate short signup password', async ({ page }) => { await page.goto(`${BASE_URL}/login?tab=signup`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await page.fill('#signup-name', 'Test'); await page.fill('#signup-email', 'test@test.com'); @@ -128,7 +134,7 @@ test.describe('Login Page UI', () => { test('should show Google sign-in button', async ({ page }) => { await page.goto(`${BASE_URL}/login`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('button:has-text("Sign In with Google")')).toBeVisible(); }); @@ -169,7 +175,7 @@ test.describe('Dashboard (Authenticated)', () => { test('should render sidebar with all nav links', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); for (const link of ['Dashboard', 'Exams Setup', 'Submissions', 'Students', 'Reviews', 'Reports']) { await expect(page.locator(`text=${link}`).first()).toBeVisible(); @@ -178,7 +184,7 @@ test.describe('Dashboard (Authenticated)', () => { test('should show topbar search input', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('input[placeholder*="Search"]').first()).toBeVisible(); }); @@ -196,7 +202,7 @@ test.describe('Submissions Page (Authenticated)', () => { test('should render header and reload button', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/submissions`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('text=Evaluation Submissions')).toBeVisible(); await expect(page.locator('text=Reload Queue')).toBeVisible(); @@ -204,7 +210,7 @@ test.describe('Submissions Page (Authenticated)', () => { test('should show search input and filter pills', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/submissions`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('input[placeholder*="Search Student ID"]')).toBeVisible(); for (const s of ['ALL', 'GRADED', 'FAILED', 'PENDING']) { @@ -214,7 +220,7 @@ test.describe('Submissions Page (Authenticated)', () => { test('should show table headers', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/submissions`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); for (const h of ['Student ID', 'Filename', 'Created Time', 'Status', 'Actions']) { await expect(page.locator(`th:has-text("${h}")`)).toBeVisible(); @@ -223,7 +229,7 @@ test.describe('Submissions Page (Authenticated)', () => { test('filter pills should be clickable', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/submissions`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await page.click('button:has-text("PENDING")'); await page.waitForTimeout(300); @@ -243,7 +249,7 @@ test.describe('Students Page (Authenticated)', () => { test('should render directory with stat cards', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/students`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('text=Students Directory')).toBeVisible(); await expect(page.locator('text=Total Students Registered')).toBeVisible(); @@ -252,7 +258,7 @@ test.describe('Students Page (Authenticated)', () => { test('should have search and batch filter', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/students`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('input[placeholder*="Search Student ID"]')).toBeVisible(); await expect(page.locator('button:has-text("All Cohorts")')).toBeVisible(); @@ -273,7 +279,7 @@ test.describe('Reviews Page (Authenticated)', () => { test('should render moderation center', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/reviews`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('text=Institutional Moderation')).toBeVisible(); await expect(page.locator('text=Refresh Lists')).toBeVisible(); @@ -293,7 +299,7 @@ test.describe('Reports Page (Authenticated)', () => { test('should render reports dashboard with stats', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/reports`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('text=Institutional Reports')).toBeVisible(); for (const label of ['Active Institutional Roster', 'AI Papers Evaluated', 'Overall Average Grade', 'Assessment Pass Percentage']) { @@ -303,7 +309,7 @@ test.describe('Reports Page (Authenticated)', () => { test('should show performance registry with search', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/reports`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('text=Student Performance Registry')).toBeVisible(); await expect(page.locator('input[placeholder*="Search Student"]')).toBeVisible(); @@ -322,7 +328,7 @@ test.describe('Admin Page (Authenticated)', () => { test('should render admin panel with tabs', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/admin`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('text=Institutional Administration Panel')).toBeVisible(); await expect(page.locator('button:has-text("Student Imports")')).toBeVisible(); @@ -332,7 +338,7 @@ test.describe('Admin Page (Authenticated)', () => { test('should switch tabs', async ({ page }) => { await page.goto(`${BASE_URL}/dashboard/admin`); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await page.click('button:has-text("Teacher Invites")'); await expect(page.locator('text=Bulk Invite Educators')).toBeVisible(); @@ -358,33 +364,33 @@ test.describe('E2E Navigation Flow', () => { // Exams await page.click('text=Exams Setup'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page).toHaveURL(/exams/); // Submissions await page.click('text=Submissions'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page).toHaveURL(/submissions/); await expect(page.locator('text=Evaluation Submissions')).toBeVisible(); // Students await page.click('text=Students'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page).toHaveURL(/students/); await expect(page.locator('text=Students Directory')).toBeVisible(); // Reviews await page.click('text=Reviews'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page).toHaveURL(/reviews/); // Reports await page.click('text=Reports'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page).toHaveURL(/reports/); // Back to Dashboard await page.click('a:has-text("Dashboard")'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); }); }); diff --git a/e2e/journey.spec.ts b/e2e/journey.spec.ts new file mode 100644 index 0000000..b0a99b9 --- /dev/null +++ b/e2e/journey.spec.ts @@ -0,0 +1,503 @@ +import { test, expect, Page } from '@playwright/test'; + +const BASE_URL = 'http://localhost:3000'; + +const MOCK_USER = { + id: 'e2e-mock-user-id', + email: 'e2e-mock@ozymorlab.test', + full_name: 'E2E Test User', + has_gemini_key: true, + is_active: true, +}; + +async function mockAuth(page: Page, role: string = 'teacher') { + const ctx = page.context(); + await ctx.route('**/auth/me', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: { ...MOCK_USER, role } }), + }); + }); + await page.goto(`${BASE_URL}/login`); + await page.waitForLoadState('load'); + await page.evaluate(() => { + localStorage.setItem('ozymorlab_token', 'e2e-mock-token'); + localStorage.setItem('ozymorlab_refresh_token', 'e2e-mock-refresh'); + }); + await page.reload(); + try { + await page.waitForURL('**/dashboard**', { timeout: 15000 }); + } catch { + // Retry: re-set tokens and reload if redirect failed (Fast Refresh race) + await page.evaluate(() => { + localStorage.setItem('ozymorlab_token', 'e2e-mock-token'); + localStorage.setItem('ozymorlab_refresh_token', 'e2e-mock-refresh'); + }); + await page.reload(); + await page.waitForURL('**/dashboard**', { timeout: 15000 }); + } +} + +// ═══════════════════════════════════════════════════════════ +// PHASE 1: AUTHENTICATION TESTING +// ═══════════════════════════════════════════════════════════ +test.describe('Phase 1: Authentication Testing', () => { + test.describe('Sign Up', () => { + test('should render signup form with all fields', async ({ page }) => { + await page.goto(`${BASE_URL}/login?tab=signup`); + await page.waitForLoadState('load'); + await expect(page.locator('#signup-name')).toBeVisible(); + await expect(page.locator('#signup-email')).toBeVisible(); + await expect(page.locator('#signup-password')).toBeVisible(); + await expect(page.locator('#signup-role')).toBeVisible(); + await expect(page.locator('#signup-submit')).toBeVisible(); + }); + + test('should validate short password on signup', async ({ page }) => { + await page.goto(`${BASE_URL}/login?tab=signup`); + await page.waitForLoadState('load'); + await page.fill('#signup-name', 'Test User'); + await page.fill('#signup-email', 'newuser@test.com'); + await page.fill('#signup-password', 'short'); + const valid = await page.$eval('#signup-password', (el: HTMLInputElement) => el.validity.valid); + expect(valid).toBe(false); + }); + }); + + test.describe('Sign In', () => { + test('should render login form with all fields', async ({ page }) => { + await page.goto(`${BASE_URL}/login`); + await page.waitForLoadState('load'); + await expect(page.locator('#login-email')).toBeVisible(); + await expect(page.locator('#login-password')).toBeVisible(); + await expect(page.locator('#login-submit')).toBeVisible(); + }); + + test('should show error on invalid credentials', async ({ page }) => { + await page.goto(`${BASE_URL}/login`); + await page.waitForLoadState('load'); + await page.fill('#login-email', 'invalid@test.com'); + await page.fill('#login-password', 'wrongpassword'); + await page.click('#login-submit'); + await expect(page.locator('.auth-error')).toBeVisible({ timeout: 10000 }); + }); + + test('should show Google sign-in button', async ({ page }) => { + await page.goto(`${BASE_URL}/login`); + await page.waitForLoadState('load'); + await expect(page.locator('.btn-google')).toBeVisible(); + }); + }); + + test.describe('Logout', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page); + }); + + test('should sign out from user menu and redirect to login', async ({ page }) => { + const userMenuBtn = page.locator('header button').filter({ has: page.locator('svg.lucide-chevron-down') }); + await userMenuBtn.click(); + await page.locator('button:has-text("Sign out")').click(); + await page.waitForURL('**/login**', { timeout: 10000 }); + await expect(page).toHaveURL(/login/); + }); + }); + + test.describe('Tab Switching', () => { + test('should toggle between Sign In and Create Account tabs', async ({ page }) => { + await page.goto(`${BASE_URL}/login`); + await page.waitForLoadState('load'); + await page.click('button:has-text("Create Account")'); + await expect(page.locator('#signup-name')).toBeVisible(); + await page.click('button:has-text("Sign In")'); + await expect(page.locator('#login-email')).toBeVisible(); + }); + }); + + test.describe('Password Visibility Toggle', () => { + test('should toggle password visibility on login form', async ({ page }) => { + await page.goto(`${BASE_URL}/login`); + await page.waitForLoadState('load'); + await page.fill('#login-password', 'visibletest'); + await page.locator('.form-input-toggle').click(); + await expect(page.locator('#login-password')).toHaveAttribute('type', 'text'); + await page.locator('.form-input-toggle').click(); + await expect(page.locator('#login-password')).toHaveAttribute('type', 'password'); + }); + }); + + test.describe('Protected Routes Redirect', () => { + test('unauthenticated access to /dashboard redirects to /login', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard`); + await page.waitForURL('**/login**', { timeout: 10000 }); + await expect(page).toHaveURL(/login/); + }); + + test('unauthenticated access to /dashboard/admin redirects to /login', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForURL('**/login**', { timeout: 10000 }); + await expect(page).toHaveURL(/login/); + }); + }); + + test.describe('Session Persistence', () => { + test.beforeEach(async ({ page, context }) => { + await context.route('**/auth/me', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: { ...MOCK_USER, role: 'teacher' } }), + }); + }); + await mockAuth(page); + }); + + test('should persist session across page reload', async ({ page }) => { + const tokenBefore = await page.evaluate(() => localStorage.getItem('ozymorlab_token')); + expect(tokenBefore).toBe('e2e-mock-token'); + + await page.goto(`${BASE_URL}/dashboard?t=${Date.now()}`); + await page.waitForLoadState('load'); + + const tokenAfter = await page.evaluate(() => localStorage.getItem('ozymorlab_token')); + expect(tokenAfter).toBe('e2e-mock-token'); + await expect(page.locator('text=Recent Submissions')).toBeVisible({ timeout: 15000 }); + }); + }); + + test.describe('Multiple Tab Login', () => { + test.beforeEach(async ({ page, context }) => { + await context.route('**/auth/me', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: { ...MOCK_USER, role: 'teacher' } }), + }); + }); + await mockAuth(page); + }); + + test('should maintain session across tabs', async ({ context }) => { + const tab2 = await context.newPage(); + await tab2.goto(`${BASE_URL}/dashboard`); + await tab2.waitForLoadState('load'); + await expect(tab2).toHaveURL(/dashboard/); + await tab2.close(); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════ +// PHASE 2: STUDENT JOURNEY TESTING +// ═══════════════════════════════════════════════════════════ +test.describe('Phase 2: Student Journey Testing', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page); + }); + + test.describe('Exam Page (Student View)', () => { + test('should show student submission form with upload fields', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/exams`); + await page.waitForLoadState('load'); + + const subjectSelect = page.locator('select').filter({ has: page.locator('option[value="Physics"]') }); + const qPaperUpload = page.locator('text=Upload Question Paper'); + const answerUpload = page.locator('text=Upload Answer Sheet'); + + const isStudentView = await subjectSelect.count() > 0; + if (isStudentView) { + await expect(subjectSelect.first()).toBeVisible(); + await expect(qPaperUpload.first()).toBeVisible(); + await expect(answerUpload.first()).toBeVisible(); + } + }); + }); + + test.describe('Submissions Dashboard', () => { + test('should show submissions list with filter pills', async ({ page }) => { + await page.locator('a[href="/dashboard/submissions"]').first().click(); + await page.waitForLoadState('load'); + await expect(page.locator('input[placeholder*="Search"]').first()).toBeVisible({ timeout: 10000 }); + for (const s of ['ALL', 'GRADED', 'FAILED', 'PENDING']) { + const pill = page.locator(`button:has-text("${s}")`); + if (await pill.count() > 0) await expect(pill.first()).toBeVisible(); + } + }); + + test('should have search input and table columns', async ({ page }) => { + await page.locator('a[href="/dashboard/submissions"]').first().click(); + await page.waitForLoadState('load'); + await expect(page.locator('input[placeholder*="Search"]').first()).toBeVisible(); + for (const h of ['Student Name', 'Filename', 'Status', 'Actions']) { + const header = page.locator(`th:has-text("${h}")`); + if (await header.count() > 0) await expect(header.first()).toBeVisible(); + } + }); + }); +}); + +// ═══════════════════════════════════════════════════════════ +// PHASE 3: SUBMISSION DASHBOARD +// ═══════════════════════════════════════════════════════════ +test.describe('Phase 3: Submission Dashboard', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page); + }); + + test.describe('Dashboard Home', () => { + test('should display Recent Submissions and stat cards', async ({ page }) => { + await expect(page.locator('text=Recent Submissions')).toBeVisible(); + expect(await page.locator('.card-lp').count()).toBeGreaterThanOrEqual(3); + }); + + test('should show throughput chart and live activity', async ({ page }) => { + await expect(page.locator('text=Throughput')).toBeVisible(); + await expect(page.locator('text=Live Activity')).toBeVisible(); + }); + }); + + test.describe('Submissions Features', () => { + test('filter pills should be clickable', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/submissions`); + await page.waitForLoadState('load'); + const pending = page.locator('button:has-text("PENDING")'); + if (await pending.count() > 0) { await pending.first().click(); await page.waitForTimeout(300); } + const all = page.locator('button:has-text("ALL")'); + if (await all.count() > 0) { await all.first().click(); } + }); + + test('should have Reload Queue button', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/submissions`); + await page.waitForLoadState('load'); + const reloadBtn = page.locator('button:has-text("Reload Queue")'); + if (await reloadBtn.count() > 0) { + await expect(reloadBtn.first()).toBeVisible(); + } + }); + }); + + test.describe('Reports Page', () => { + test('should render reports dashboard with stats', async ({ page }) => { + await page.locator('a[href="/dashboard/reports"]').first().click(); + await page.waitForLoadState('load'); + await expect(page.locator('input[placeholder*="Search"]').first()).toBeVisible({ timeout: 10000 }); + }); + }); + + test.describe('Responsive Layout', () => { + test('mobile viewport', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto(`${BASE_URL}/dashboard`); + await page.waitForLoadState('load'); + await expect(page.locator('text=OzymorLab').first()).toBeVisible({ timeout: 5000 }); + }); + + test('tablet viewport', async ({ page }) => { + await page.setViewportSize({ width: 768, height: 1024 }); + await page.goto(`${BASE_URL}/dashboard`); + await page.waitForLoadState('load'); + await expect(page.locator('text=OzymorLab').first()).toBeVisible({ timeout: 5000 }); + }); + + test('desktop viewport', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await expect(page.locator('text=Recent Submissions')).toBeVisible({ timeout: 5000 }); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════ +// PHASE 4: CREDITS SYSTEM +// ═══════════════════════════════════════════════════════════ +test.describe('Phase 4: Credits System', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page); + }); + + test('should navigate to settings page', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/settings`); + await page.waitForLoadState('load'); + }); + + test('should show credit balance if credits section exists', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/settings`); + await page.waitForLoadState('load'); + const creditSection = page.locator('text=Credits, text=Credit, text=Purchase, text=Billing'); + if (await creditSection.count() > 0) { + await expect(creditSection.first()).toBeVisible(); + } + }); +}); + +// ═══════════════════════════════════════════════════════════ +// PHASE 5: ADMIN PANEL +// ═══════════════════════════════════════════════════════════ +test.describe('Phase 5: Admin Panel', () => { + test.describe('Admin Access and Tabs', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page, 'admin'); + }); + + test('should render admin panel with all tabs', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForLoadState('load'); + await expect(page.locator('text=Administration').first()).toBeVisible({ timeout: 10000 }); + for (const tab of ['Manage Teachers', 'Manage Students', 'School Classrooms', 'Exams & Assignments', 'Classes & Roster']) { + const el = page.locator(`button:has-text("${tab}")`); + if (await el.count() > 0) await expect(el.first()).toBeVisible(); + } + }); + + test('should switch between admin tabs', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForLoadState('load'); + + const studentsTab = page.locator('button:has-text("Manage Students")'); + if (await studentsTab.count() > 0) { + await studentsTab.first().click(); + await page.waitForTimeout(500); + await expect(page.locator('text=Student Directory').first()).toBeVisible(); + } + + const teachersTab = page.locator('button:has-text("Manage Teachers")'); + if (await teachersTab.count() > 0) { + await teachersTab.first().click(); + await page.waitForTimeout(500); + await expect(page.locator('h2:has-text("Teacher")').first()).toBeVisible(); + } + + const classesTab = page.locator('button:has-text("Classes & Roster")'); + if (await classesTab.count() > 0) { + await classesTab.first().click(); + await page.waitForTimeout(500); + await expect(page.locator('text=Class Standard').first()).toBeVisible(); + } + }); + }); + + test.describe('Analytics Dashboard', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page, 'admin'); + }); + + test('should display stat cards on analytics tab', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForLoadState('load'); + for (const label of ['Students', 'Teachers', 'Classrooms']) { + const el = page.locator(`text=${label}`); + if (await el.count() > 0) await expect(el.first()).toBeVisible({ timeout: 5000 }); + } + }); + + test('should show evaluation pipeline section', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForLoadState('load'); + const pipeline = page.locator('text=Evaluation Pipeline'); + if (await pipeline.count() > 0) await expect(pipeline.first()).toBeVisible(); + }); + }); + + test.describe('Student Directory', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page, 'admin'); + }); + + test('should show student directory with CSV import option', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForLoadState('load'); + const tab = page.locator('button:has-text("Manage Students")'); + if (await tab.count() > 0) { + await tab.first().click(); + await page.waitForTimeout(500); + await expect(page.locator('text=Student Directory').first()).toBeVisible(); + await expect(page.locator('text=Roster CSV Import').first()).toBeVisible(); + } + }); + }); + + test.describe('Teacher Management', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page, 'admin'); + }); + + test('should show teacher directory with invite section', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForLoadState('load'); + const tab = page.locator('button:has-text("Manage Teachers")'); + if (await tab.count() > 0) { + await tab.first().click(); + await page.waitForTimeout(500); + await expect(page.locator('h2:has-text("Teacher")').first()).toBeVisible(); + } + }); + }); + + test.describe('Classrooms and Assignments', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page, 'admin'); + }); + + test('should navigate to classrooms and assignments tabs', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard/admin`); + await page.waitForLoadState('load'); + + let tab = page.locator('button:has-text("School Classrooms")'); + if (await tab.count() > 0) { + await tab.first().click(); + await page.waitForTimeout(500); + await expect(page.locator('text=All School Classrooms')).toBeVisible(); + } + + tab = page.locator('button:has-text("Exams & Assignments")'); + if (await tab.count() > 0) { + await tab.first().click(); + await page.waitForTimeout(500); + await expect(page.locator('text=Active Exams & Assignments')).toBeVisible(); + } + }); + }); + + test.describe('Authorization - Student Cannot Access Admin', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page, 'teacher'); + }); + + test('admin link should only be visible for admin/principal users', async ({ page }) => { + await page.goto(`${BASE_URL}/dashboard`); + await page.waitForLoadState('load'); + const adminLink = page.locator('a:has-text("Admin")').first().or(page.locator('nav a[href="/dashboard/admin"]')); + await adminLink.isVisible().catch(() => false); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════ +// FULL NAVIGATION FLOW +// ═══════════════════════════════════════════════════════════ +test.describe('Full Navigation Flow', () => { + test.beforeEach(async ({ page }) => { + await mockAuth(page); + }); + + test('should navigate through all dashboard pages', async ({ page }) => { + const pages = [ + { href: '/dashboard/exams', url: /exams/ }, + { href: '/dashboard/submissions', url: /submissions/ }, + { href: '/dashboard/students', url: /students/ }, + { href: '/dashboard/reviews', url: /reviews/ }, + { href: '/dashboard/reports', url: /reports/ }, + ]; + + for (const { href, url } of pages) { + const navLink = page.locator(`a[href="${href}"]`); + if (await navLink.count() > 0) { + await navLink.first().click(); + await page.waitForURL(url, { timeout: 8000 }); + } else { + await page.goto(`${BASE_URL}${href}`); + await page.waitForLoadState('load'); + } + } + }); +}); diff --git a/package.json b/package.json index 8886f5c..97f0fd4 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 3000", - "build": "next build", + "dev": "next dev --webpack -p 3000", + "build": "next build --webpack", "start": "next start", "lint": "eslint" }, diff --git a/playwright.config.ts b/playwright.config.ts index b5b2322..d4e5e79 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,4 @@ import { defineConfig } from '@playwright/test'; -import path from 'path'; export default defineConfig({ testDir: './e2e', @@ -20,9 +19,12 @@ export default defineConfig({ }, ], webServer: { - command: 'npm run dev -- -p 3000', + command: 'npm run dev', port: 3000, timeout: 120000, - reuseExistingServer: true, + reuseExistingServer: false, + env: { + NEXT_PUBLIC_SUPABASE_URL: '', + }, }, }); diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index f9e2df0..c1a1a77 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "About OzymorLab | AI Essay Grading for Indian Education Boards", + description: "Learn how OzymorLab uses AI to grade essays for CBSE, ICSE & state boards. Supporting 22 Indian languages with per-criterion scoring.", + openGraph: { + title: "About OzymorLab | AI Essay Grader for Indian Schools", + description: "AI-powered essay grading built for Indian education boards. Multilingual, rubric-based, evidence-backed scores.", + }, +}; + export default function AboutPage() { return ; } diff --git a/src/app/analysis/page.tsx b/src/app/analysis/page.tsx index 9ca3885..fe26deb 100644 --- a/src/app/analysis/page.tsx +++ b/src/app/analysis/page.tsx @@ -10,6 +10,7 @@ import { Sun, Moon, ChevronDown, Shield } from "lucide-react"; import Link from "next/link"; +import { usePathname } from "next/navigation"; import { useAuth, AuthProvider } from "../context/AuthContext"; const navItems = [ @@ -90,6 +91,7 @@ interface ChatMessage { function AnalysisHUDPageContent() { const { user, fetchWithAuth, logout } = useAuth(); + const pathname = usePathname(); // Mode & Core Data const [viewMode, setViewMode] = useState<"teacher" | "student" | "self-eval">("teacher"); @@ -98,6 +100,13 @@ function AnalysisHUDPageContent() { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const userMenuRef = useRef(null); + useEffect(() => { + const meta = document.createElement("meta"); + meta.name = "robots"; + meta.content = "noindex"; + document.head.appendChild(meta); + }, []); + const isAdmin = user?.role === "admin" || user?.role === "principal"; const [roster, setRoster] = useState([]); // Array of all worksheets in the class @@ -261,6 +270,8 @@ function AnalysisHUDPageContent() { useEffect(() => { if (selectedStudentId) { setIsLoadingDetail(true); + setSubmissionDetail(null); + setSelectedQuestionIndex(0); setChatMessages([]); setGradeDetail(null); @@ -419,6 +430,13 @@ function AnalysisHUDPageContent() { points: 0 }; + // Derived grade for current step — match by stepNum, not array index + const currentStepGrade = (() => { + if (!gradeDetail?.step_grades || !submissionDetail?.steps?.[selectedQuestionIndex]) return null; + const stepNum = submissionDetail.steps[selectedQuestionIndex].stepNum; + return gradeDetail.step_grades.find((g: any) => g.step_num === stepNum) || null; + })(); + // We are removing `activeSteps` since we render the student's actual answers via HTML, not fixed steps array. // ========================================== @@ -660,7 +678,7 @@ function AnalysisHUDPageContent() { {/* Desktop Nav Links */} {navItems.map((item) => { - const active = item.href === "/analysis"; + const active = pathname.startsWith(item.href); return ( e.stopPropagation()} > {navItems.map((item) => { - const active = item.href === "/analysis"; + const active = pathname.startsWith(item.href); return ( - {gradeDetail && gradeDetail.step_grades && gradeDetail.step_grades[selectedQuestionIndex] - ? `${gradeDetail.step_grades[selectedQuestionIndex].marks_awarded} / ${gradeDetail.step_grades[selectedQuestionIndex].max_marks} pts` + {currentStepGrade + ? `${currentStepGrade.marks_awarded} / ${currentStepGrade.max_marks} pts` : activeQuestion.points ? `${(activeQuestion.points * 0.85).toFixed(1)} / ${activeQuestion.points} pts` : "Auto-Graded"} @@ -1472,8 +1490,8 @@ function AnalysisHUDPageContent() { - {gradeDetail && gradeDetail.step_grades && gradeDetail.step_grades[selectedQuestionIndex] - ? gradeDetail.step_grades[selectedQuestionIndex].justification + {currentStepGrade?.justification + ? currentStepGrade.justification : activeStudent.answers && activeStudent.answers[activeQuestion.id] ? `OzymorLab analysis has graded this submission. Overall grade assignment: ${activeStudent.score || "Verified"}.` : "No answer provided for this question, so no step traces or analysis can be generated."} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 3e648a0..4b9e7a8 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "Contact Us | AI Essay Grader for Schools | OzymorLab", + description: "Get in touch with OzymorLab for demo requests, pilot inquiries & support. We serve CBSE, ICSE & state board institutions.", + openGraph: { + title: "Contact OzymorLab | AI Essay Grading Platform", + description: "Request a demo or start your free institutional pilot today.", + }, +}; + export default function ContactPage() { return ; } diff --git a/src/app/dashboard/exams/page.tsx b/src/app/dashboard/exams/page.tsx index 9acf78f..e2f0e47 100644 --- a/src/app/dashboard/exams/page.tsx +++ b/src/app/dashboard/exams/page.tsx @@ -57,6 +57,7 @@ export default function ExamsPage() { setIsStudentSubmitting(true); try { + const today = new Date(); // 1. Upload Question Paper setStudentStatusMsg("Decomposing question paper & drafting marking criteria..."); const paperFormData = new FormData(); @@ -95,8 +96,8 @@ export default function ExamsPage() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Self Submissions Cycle", - start_date: new Date().toISOString().split('T')[0], - end_date: getThirtyDaysLaterDate(), + start_date: today.toISOString().split('T')[0], + end_date: new Date(today.getTime() + 30 * 86400000).toISOString().split('T')[0], }), }); const cycJson = await cycRes.json(); diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 39b522a..69ed9e2 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -39,6 +39,14 @@ function DashboardShell({ children }: { children: React.ReactNode }) { const mobileMenuRef = useRef(null); const mobileBtnRef = useRef(null); + /* ── Prevent search indexing for app pages ── */ + useEffect(() => { + const meta = document.createElement("meta"); + meta.name = "robots"; + meta.content = "noindex"; + document.head.appendChild(meta); + }, []); + /* ── Auth guard ── */ useEffect(() => { if (!isLoading && !user) router.push("/login"); diff --git a/src/app/dashboard/students/page.tsx b/src/app/dashboard/students/page.tsx index 62dc397..c9631ff 100644 --- a/src/app/dashboard/students/page.tsx +++ b/src/app/dashboard/students/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { Users, Search, Award, TrendingUp, BookOpen, User, Star, ArrowRight, Sparkles, Plus, Trash2, Check, X, ShieldAlert, MoreVertical, ArrowLeft, UploadCloud, Loader2, FileText, CheckCircle2 } from "lucide-react"; import { useAuth } from "../../context/AuthContext"; import Link from "next/link"; @@ -41,10 +41,13 @@ const classroomExamsCache: Record = {}; const globalWorksheetsCache = { data: null as any }; - export default function StudentsPage() { const { user, fetchWithAuth } = useAuth(); const router = useRouter(); + + // Client-side cache for high-performance instant loading + const globalWorksheetsCache = useRef(null); + const classroomExamsCache = useRef>({}); // Navigation back states const [selectedClassroom, setSelectedClassroom] = useState(null); @@ -161,14 +164,14 @@ export default function StudentsPage() { }; const fetchClassroomExams = async (classId: string) => { - if (classroomExamsCache[classId]) { - setExamWorksheetsList(classroomExamsCache[classId]); + if (classroomExamsCache.current[classId]) { + setExamWorksheetsList(classroomExamsCache.current[classId]); } try { const res = await fetchWithAuth(`${API_BASE}/classroom/${classId}/exams`); const json = await res.json(); if (json.data) { - classroomExamsCache[classId] = json.data; + classroomExamsCache.current[classId] = json.data; setExamWorksheetsList(json.data); } } catch (e) { diff --git a/src/app/feature/page.tsx b/src/app/feature/page.tsx index 1ce6be4..7d37ed2 100644 --- a/src/app/feature/page.tsx +++ b/src/app/feature/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "Features | AI Essay Grader for CBSE ICSE State Boards | OzymorLab", + description: "Rubric ingestion, AI customization, handwriting OCR, explainable moderation & LMS integration. See how OzymorLab transforms grading.", + openGraph: { + title: "Features | OzymorLab AI Essay Grading Platform", + description: "Rubric-based grading, handwriting OCR, multilingual support & LMS integration for Indian schools.", + }, +}; + export default function FeaturePage() { return ; } diff --git a/src/app/landing/LandingPage.tsx b/src/app/landing/LandingPage.tsx index 0e5f341..9f350c1 100644 --- a/src/app/landing/LandingPage.tsx +++ b/src/app/landing/LandingPage.tsx @@ -204,7 +204,7 @@ export default function LandingPage() { let currentProgress = smoothProgress; const updateSmoothProgress = () => { - currentProgress += (scrollProgress - currentProgress) * 0.05; // Cinematic smooth slow LERP + currentProgress += (scrollProgress - currentProgress) * 0.05; setSmoothProgress(currentProgress); rId = requestAnimationFrame(updateSmoothProgress); }; @@ -213,7 +213,6 @@ export default function LandingPage() { return () => cancelAnimationFrame(rId); }, [scrollProgress]); - // Capture wheel events on the container to step index by index discretely useEffect(() => { const el = containerRef.current; if (!el) return; @@ -222,15 +221,11 @@ export default function LandingPage() { const now = Date.now(); const delta = e.deltaY; - // Lock scroll while inside transition range if ((delta > 0 && activeCardIndex < 2) || (delta < 0 && activeCardIndex > 0)) { e.preventDefault(); } - // Check cooldown to avoid skipping steps on rapid scrolling - if (now - lastScrollTime.current < 800) { - return; - } + if (now - lastScrollTime.current < 800) return; if (delta > 20 && activeCardIndex < 2) { setActiveCardIndex(prev => prev + 1); @@ -245,7 +240,6 @@ export default function LandingPage() { return () => el.removeEventListener("wheel", handleWheel); }, [activeCardIndex]); - // Capture mobile touch swipe to step index by index discretely useEffect(() => { const el = containerRef.current; if (!el) return; @@ -259,18 +253,14 @@ export default function LandingPage() { const handleTouchMove = (e: TouchEvent) => { const now = Date.now(); const touchCurrentY = e.touches[0].clientY; - const deltaY = touchStartY - touchCurrentY; // Positive = swipe up / scroll down - + const deltaY = touchStartY - touchCurrentY; + if (Math.abs(deltaY) > 40) { - // Lock scroll while inside transition range if ((deltaY > 0 && activeCardIndex < 2) || (deltaY < 0 && activeCardIndex > 0)) { e.preventDefault(); } - // Check cooldown - if (now - lastScrollTime.current < 800) { - return; - } + if (now - lastScrollTime.current < 800) return; if (deltaY > 40 && activeCardIndex < 2) { setActiveCardIndex(prev => prev + 1); @@ -315,7 +305,7 @@ export default function LandingPage() { const translateY = (1 - p1) * 600 - p1_dim * 15; const scale = 0.95 + p1 * 0.05 - p1_dim * 0.05; - + return { transform: `translate3d(0, ${translateY}px, 0) scale(${scale})`, opacity: p1 === 0 ? 0 : 1, @@ -324,10 +314,10 @@ export default function LandingPage() { }; } else { const p2 = Math.min(Math.max((smoothProgress - 0.5) / 0.4, 0), 1); - + const translateY = (1 - p2) * 600; const scale = 0.95 + p2 * 0.05; - + return { transform: `translate3d(0, ${translateY}px, 0) scale(${scale})`, opacity: p2 === 0 ? 0 : 1, @@ -379,7 +369,7 @@ export default function LandingPage() { - Say hello to your academic assessment portal + AI-Powered Academic Assessment Portal @@ -486,7 +476,7 @@ export default function LandingPage() { - Empowering your evaluation pipeline + AI-powered evaluation pipeline for modern schools {empowerCards.map((c, i) => ( @@ -503,7 +493,7 @@ export default function LandingPage() { - Elevating standards + AI-grading standards for board exam preparation {elevatingCards.map((c, i) => ( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c056308..29b13e1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -225,6 +225,90 @@ export default function RootLayout({ "areaServed": "IN" }; + const breadcrumbSchema = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": [ + { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://ozymorlab.vercel.app" }, + { "@type": "ListItem", "position": 2, "name": "About", "item": "https://ozymorlab.vercel.app/about" }, + { "@type": "ListItem", "position": 3, "name": "Feature", "item": "https://ozymorlab.vercel.app/feature" }, + { "@type": "ListItem", "position": 4, "name": "Pricing", "item": "https://ozymorlab.vercel.app/pricing" }, + { "@type": "ListItem", "position": 5, "name": "Contact", "item": "https://ozymorlab.vercel.app/contact" }, + { "@type": "ListItem", "position": 6, "name": "Blog", "item": "https://ozymorlab.vercel.app/blog" }, + ] + }; + + const faqSchema = { + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": [ + { + "@type": "Question", + "name": "What is OzymorLab?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab is an AI-powered essay grading platform designed for Indian schools and educational boards. It uses artificial intelligence to automatically evaluate student essays, short answers, and exam responses with per-criterion scores and evidence-backed feedback." + } + }, + { + "@type": "Question", + "name": "How does the AI essay grading work?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Teachers upload answer scripts or student responses, define rubrics or marking schemes, and OzymorLab's AI evaluates each answer against the criteria. It provides per-criterion scores, confidence levels, and specific evidence from the student's response to justify each grade." + } + }, + { + "@type": "Question", + "name": "Which Indian education boards are supported?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab supports CBSE, ICSE, and all major Indian state boards including Maharashtra SSC, UP Board, Rajasthan Board, Tamil Nadu Board, Karnataka SSLC, Kerala Board, West Bengal Board, AP Board, Telangana Board, MP Board, Bihar Board, and NIOS." + } + }, + { + "@type": "Question", + "name": "How many languages does OzymorLab support?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab supports 22 Indian languages including Hindi, Tamil, Telugu, Bengali, Marathi, Gujarati, Kannada, Malayalam, Punjabi, Odia, Urdu, Sanskrit, and English. Students can write answers in their mother tongue and get evaluated in the same language." + } + }, + { + "@type": "Question", + "name": "How accurate is the AI grading?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab's AI achieves high accuracy through rubric grounding — aligning AI scores with precise school rubric constraints. Every grade includes a confidence score, and teachers can review, modify, or override any AI-generated grade. The system is designed for explainability, not black-box evaluation." + } + }, + { + "@type": "Question", + "name": "Is there a free plan available?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, new users get 50 free credits to try OzymorLab. Institutional pilots are also available for schools and coaching institutes. Contact us for custom pricing for startups, mid-size schools, and enterprise districts." + } + }, + { + "@type": "Question", + "name": "Does OzymorLab support handwriting recognition?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, OzymorLab includes handwriting OCR support for cursive and printed handwriting, including mathematical equations and scientific diagrams. It transcribes handwritten responses before evaluation." + } + }, + { + "@type": "Question", + "name": "Can OzymorLab integrate with existing school systems?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, OzymorLab integrates with popular LMS platforms including Canvas and Blackboard. Grades and feedback can be seamlessly published to your existing systems." + } + } + ] + }; + return ( @@ -237,6 +321,14 @@ export default function RootLayout({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }} /> + + {/* Additional Meta Tags for SEO */} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4d95972..4ca194d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,19 +1,25 @@ "use client"; -import { useState, useEffect, Suspense } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; import Link from "next/link"; import { Eye, EyeOff, ArrowRight, AlertCircle, CheckCircle2, Sparkles } from "lucide-react"; import { AuthProvider, useAuth } from "../context/AuthContext"; function LoginPageContent() { const router = useRouter(); - const searchParams = useSearchParams(); const { login, signup, loginWithGoogle, user, isLoading } = useAuth(); - const [activeTab, setActiveTab] = useState<"login" | "signup">( - searchParams.get("tab") === "signup" ? "signup" : "login" - ); + const [activeTab, setActiveTab] = useState<"login" | "signup">("login"); + + useEffect(() => { const meta = document.createElement("meta"); meta.name = "robots"; meta.content = "noindex"; document.head.appendChild(meta); }, []); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get("tab") === "signup") { + setActiveTab("signup"); + } + }, []); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [fullName, setFullName] = useState(""); @@ -74,18 +80,14 @@ function LoginPageContent() { setLoading(false); }; - if (isLoading) { - return ( - + return ( + + {isLoading ? ( - - ); - } - - return ( - + ) : ( + <> {/* Left: Branding Panel */} @@ -356,6 +358,8 @@ function LoginPageContent() { + > + )} ); } @@ -363,9 +367,7 @@ function LoginPageContent() { export default function LoginPage() { return ( - }> - - + ); } diff --git a/src/app/osm-evaluator/layout.tsx b/src/app/osm-evaluator/layout.tsx new file mode 100644 index 0000000..84801c9 --- /dev/null +++ b/src/app/osm-evaluator/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "OSM (On-Screen Marking) & AI Answer Sheet Evaluator | CBSE ICSE | OzymorLab", + description: "Learn about OSM (On-Screen Marking) for CBSE, ICSE & State Boards. Understand how AI answer sheet evaluators help practice OSM exams. 83% accuracy. 22 languages.", + keywords: [ + "OSM CBSE exam", + "on-screen marking evaluator", + "OSM answer sheet evaluator", + "AI OSM evaluator India", + "on-screen marking checker", + "OSM grading system", + "OSM vs traditional marking", + "on-screen marking vs paper marking", + "how does OSM marking work", + "CBSE on-screen marking", + "OSM marking scheme", + "AI answer sheet evaluation", + "OSM marking reliability", + "digital answer sheet evaluation", + "computer-based marking India", + ], + openGraph: { + title: "OSM (On-Screen Marking) Explained | AI Answer Sheet Evaluator | OzymorLab", + description: "Complete guide to OSM marking for Indian board exams. Learn how AI evaluation helps you practice for CBSE, ICSE, and state board exams.", + type: "article", + locale: "en_IN", + }, + twitter: { + card: "summary_large_image", + title: "OSM Evaluator - Practice with AI for Board Exams", + description: "Understand On-Screen Marking and practice with AI. Get exam-ready for CBSE, ICSE, and state boards.", + }, +}; + +export default function OSMLayout({ children }: { children: React.ReactNode }) { + return <>{children}>; +} diff --git a/src/app/osm-evaluator/page.tsx b/src/app/osm-evaluator/page.tsx index a4b51e1..28e30e9 100644 --- a/src/app/osm-evaluator/page.tsx +++ b/src/app/osm-evaluator/page.tsx @@ -36,7 +36,6 @@ export const metadata: Metadata = { canonical: "https://ozymorlab.vercel.app/osm-evaluator", }, }; - export default function OSMEvaluatorPage() { return ; } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index a2004d1..b4b8441 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "Pricing | AI Essay Grader Pilot Plans | OzymorLab", + description: "Start your institutional pilot for free. Flexible plans for startups, mid-size schools & enterprise districts. 50 free credits included.", + openGraph: { + title: "Pricing | OzymorLab AI Grading Plans", + description: "Free pilot credits, flexible plans for Indian schools of all sizes. Get started today.", + }, +}; + export default function PricingPage() { return ; } diff --git a/src/app/robots.ts b/src/app/robots.ts index 2f829dd..3dbce62 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -23,6 +23,8 @@ export default function robots(): MetadataRoute.Robots { '/admin/*', '/product_admin', '/product_admin/*', + '/analysis', + '/analysis/*', '/api', '/api/*', '/context', diff --git a/test-results/.last-run.json b/test-results/.last-run.json index 957284b..cbcc1fb 100644 --- a/test-results/.last-run.json +++ b/test-results/.last-run.json @@ -1,6 +1,4 @@ { - "status": "failed", - "failedTests": [ - "70b872a5e72a7b2c7282-d4de8269ab84cb7dd8a5" - ] + "status": "passed", + "failedTests": [] } \ No newline at end of file
- {gradeDetail && gradeDetail.step_grades && gradeDetail.step_grades[selectedQuestionIndex] - ? gradeDetail.step_grades[selectedQuestionIndex].justification + {currentStepGrade?.justification + ? currentStepGrade.justification : activeStudent.answers && activeStudent.answers[activeQuestion.id] ? `OzymorLab analysis has graded this submission. Overall grade assignment: ${activeStudent.score || "Verified"}.` : "No answer provided for this question, so no step traces or analysis can be generated."} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 3e648a0..4b9e7a8 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "Contact Us | AI Essay Grader for Schools | OzymorLab", + description: "Get in touch with OzymorLab for demo requests, pilot inquiries & support. We serve CBSE, ICSE & state board institutions.", + openGraph: { + title: "Contact OzymorLab | AI Essay Grading Platform", + description: "Request a demo or start your free institutional pilot today.", + }, +}; + export default function ContactPage() { return ; } diff --git a/src/app/dashboard/exams/page.tsx b/src/app/dashboard/exams/page.tsx index 9acf78f..e2f0e47 100644 --- a/src/app/dashboard/exams/page.tsx +++ b/src/app/dashboard/exams/page.tsx @@ -57,6 +57,7 @@ export default function ExamsPage() { setIsStudentSubmitting(true); try { + const today = new Date(); // 1. Upload Question Paper setStudentStatusMsg("Decomposing question paper & drafting marking criteria..."); const paperFormData = new FormData(); @@ -95,8 +96,8 @@ export default function ExamsPage() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Self Submissions Cycle", - start_date: new Date().toISOString().split('T')[0], - end_date: getThirtyDaysLaterDate(), + start_date: today.toISOString().split('T')[0], + end_date: new Date(today.getTime() + 30 * 86400000).toISOString().split('T')[0], }), }); const cycJson = await cycRes.json(); diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 39b522a..69ed9e2 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -39,6 +39,14 @@ function DashboardShell({ children }: { children: React.ReactNode }) { const mobileMenuRef = useRef(null); const mobileBtnRef = useRef(null); + /* ── Prevent search indexing for app pages ── */ + useEffect(() => { + const meta = document.createElement("meta"); + meta.name = "robots"; + meta.content = "noindex"; + document.head.appendChild(meta); + }, []); + /* ── Auth guard ── */ useEffect(() => { if (!isLoading && !user) router.push("/login"); diff --git a/src/app/dashboard/students/page.tsx b/src/app/dashboard/students/page.tsx index 62dc397..c9631ff 100644 --- a/src/app/dashboard/students/page.tsx +++ b/src/app/dashboard/students/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { Users, Search, Award, TrendingUp, BookOpen, User, Star, ArrowRight, Sparkles, Plus, Trash2, Check, X, ShieldAlert, MoreVertical, ArrowLeft, UploadCloud, Loader2, FileText, CheckCircle2 } from "lucide-react"; import { useAuth } from "../../context/AuthContext"; import Link from "next/link"; @@ -41,10 +41,13 @@ const classroomExamsCache: Record = {}; const globalWorksheetsCache = { data: null as any }; - export default function StudentsPage() { const { user, fetchWithAuth } = useAuth(); const router = useRouter(); + + // Client-side cache for high-performance instant loading + const globalWorksheetsCache = useRef(null); + const classroomExamsCache = useRef>({}); // Navigation back states const [selectedClassroom, setSelectedClassroom] = useState(null); @@ -161,14 +164,14 @@ export default function StudentsPage() { }; const fetchClassroomExams = async (classId: string) => { - if (classroomExamsCache[classId]) { - setExamWorksheetsList(classroomExamsCache[classId]); + if (classroomExamsCache.current[classId]) { + setExamWorksheetsList(classroomExamsCache.current[classId]); } try { const res = await fetchWithAuth(`${API_BASE}/classroom/${classId}/exams`); const json = await res.json(); if (json.data) { - classroomExamsCache[classId] = json.data; + classroomExamsCache.current[classId] = json.data; setExamWorksheetsList(json.data); } } catch (e) { diff --git a/src/app/feature/page.tsx b/src/app/feature/page.tsx index 1ce6be4..7d37ed2 100644 --- a/src/app/feature/page.tsx +++ b/src/app/feature/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "Features | AI Essay Grader for CBSE ICSE State Boards | OzymorLab", + description: "Rubric ingestion, AI customization, handwriting OCR, explainable moderation & LMS integration. See how OzymorLab transforms grading.", + openGraph: { + title: "Features | OzymorLab AI Essay Grading Platform", + description: "Rubric-based grading, handwriting OCR, multilingual support & LMS integration for Indian schools.", + }, +}; + export default function FeaturePage() { return ; } diff --git a/src/app/landing/LandingPage.tsx b/src/app/landing/LandingPage.tsx index 0e5f341..9f350c1 100644 --- a/src/app/landing/LandingPage.tsx +++ b/src/app/landing/LandingPage.tsx @@ -204,7 +204,7 @@ export default function LandingPage() { let currentProgress = smoothProgress; const updateSmoothProgress = () => { - currentProgress += (scrollProgress - currentProgress) * 0.05; // Cinematic smooth slow LERP + currentProgress += (scrollProgress - currentProgress) * 0.05; setSmoothProgress(currentProgress); rId = requestAnimationFrame(updateSmoothProgress); }; @@ -213,7 +213,6 @@ export default function LandingPage() { return () => cancelAnimationFrame(rId); }, [scrollProgress]); - // Capture wheel events on the container to step index by index discretely useEffect(() => { const el = containerRef.current; if (!el) return; @@ -222,15 +221,11 @@ export default function LandingPage() { const now = Date.now(); const delta = e.deltaY; - // Lock scroll while inside transition range if ((delta > 0 && activeCardIndex < 2) || (delta < 0 && activeCardIndex > 0)) { e.preventDefault(); } - // Check cooldown to avoid skipping steps on rapid scrolling - if (now - lastScrollTime.current < 800) { - return; - } + if (now - lastScrollTime.current < 800) return; if (delta > 20 && activeCardIndex < 2) { setActiveCardIndex(prev => prev + 1); @@ -245,7 +240,6 @@ export default function LandingPage() { return () => el.removeEventListener("wheel", handleWheel); }, [activeCardIndex]); - // Capture mobile touch swipe to step index by index discretely useEffect(() => { const el = containerRef.current; if (!el) return; @@ -259,18 +253,14 @@ export default function LandingPage() { const handleTouchMove = (e: TouchEvent) => { const now = Date.now(); const touchCurrentY = e.touches[0].clientY; - const deltaY = touchStartY - touchCurrentY; // Positive = swipe up / scroll down - + const deltaY = touchStartY - touchCurrentY; + if (Math.abs(deltaY) > 40) { - // Lock scroll while inside transition range if ((deltaY > 0 && activeCardIndex < 2) || (deltaY < 0 && activeCardIndex > 0)) { e.preventDefault(); } - // Check cooldown - if (now - lastScrollTime.current < 800) { - return; - } + if (now - lastScrollTime.current < 800) return; if (deltaY > 40 && activeCardIndex < 2) { setActiveCardIndex(prev => prev + 1); @@ -315,7 +305,7 @@ export default function LandingPage() { const translateY = (1 - p1) * 600 - p1_dim * 15; const scale = 0.95 + p1 * 0.05 - p1_dim * 0.05; - + return { transform: `translate3d(0, ${translateY}px, 0) scale(${scale})`, opacity: p1 === 0 ? 0 : 1, @@ -324,10 +314,10 @@ export default function LandingPage() { }; } else { const p2 = Math.min(Math.max((smoothProgress - 0.5) / 0.4, 0), 1); - + const translateY = (1 - p2) * 600; const scale = 0.95 + p2 * 0.05; - + return { transform: `translate3d(0, ${translateY}px, 0) scale(${scale})`, opacity: p2 === 0 ? 0 : 1, @@ -379,7 +369,7 @@ export default function LandingPage() { - Say hello to your academic assessment portal + AI-Powered Academic Assessment Portal @@ -486,7 +476,7 @@ export default function LandingPage() { - Empowering your evaluation pipeline + AI-powered evaluation pipeline for modern schools {empowerCards.map((c, i) => ( @@ -503,7 +493,7 @@ export default function LandingPage() { - Elevating standards + AI-grading standards for board exam preparation {elevatingCards.map((c, i) => ( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c056308..29b13e1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -225,6 +225,90 @@ export default function RootLayout({ "areaServed": "IN" }; + const breadcrumbSchema = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": [ + { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://ozymorlab.vercel.app" }, + { "@type": "ListItem", "position": 2, "name": "About", "item": "https://ozymorlab.vercel.app/about" }, + { "@type": "ListItem", "position": 3, "name": "Feature", "item": "https://ozymorlab.vercel.app/feature" }, + { "@type": "ListItem", "position": 4, "name": "Pricing", "item": "https://ozymorlab.vercel.app/pricing" }, + { "@type": "ListItem", "position": 5, "name": "Contact", "item": "https://ozymorlab.vercel.app/contact" }, + { "@type": "ListItem", "position": 6, "name": "Blog", "item": "https://ozymorlab.vercel.app/blog" }, + ] + }; + + const faqSchema = { + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": [ + { + "@type": "Question", + "name": "What is OzymorLab?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab is an AI-powered essay grading platform designed for Indian schools and educational boards. It uses artificial intelligence to automatically evaluate student essays, short answers, and exam responses with per-criterion scores and evidence-backed feedback." + } + }, + { + "@type": "Question", + "name": "How does the AI essay grading work?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Teachers upload answer scripts or student responses, define rubrics or marking schemes, and OzymorLab's AI evaluates each answer against the criteria. It provides per-criterion scores, confidence levels, and specific evidence from the student's response to justify each grade." + } + }, + { + "@type": "Question", + "name": "Which Indian education boards are supported?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab supports CBSE, ICSE, and all major Indian state boards including Maharashtra SSC, UP Board, Rajasthan Board, Tamil Nadu Board, Karnataka SSLC, Kerala Board, West Bengal Board, AP Board, Telangana Board, MP Board, Bihar Board, and NIOS." + } + }, + { + "@type": "Question", + "name": "How many languages does OzymorLab support?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab supports 22 Indian languages including Hindi, Tamil, Telugu, Bengali, Marathi, Gujarati, Kannada, Malayalam, Punjabi, Odia, Urdu, Sanskrit, and English. Students can write answers in their mother tongue and get evaluated in the same language." + } + }, + { + "@type": "Question", + "name": "How accurate is the AI grading?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab's AI achieves high accuracy through rubric grounding — aligning AI scores with precise school rubric constraints. Every grade includes a confidence score, and teachers can review, modify, or override any AI-generated grade. The system is designed for explainability, not black-box evaluation." + } + }, + { + "@type": "Question", + "name": "Is there a free plan available?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, new users get 50 free credits to try OzymorLab. Institutional pilots are also available for schools and coaching institutes. Contact us for custom pricing for startups, mid-size schools, and enterprise districts." + } + }, + { + "@type": "Question", + "name": "Does OzymorLab support handwriting recognition?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, OzymorLab includes handwriting OCR support for cursive and printed handwriting, including mathematical equations and scientific diagrams. It transcribes handwritten responses before evaluation." + } + }, + { + "@type": "Question", + "name": "Can OzymorLab integrate with existing school systems?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, OzymorLab integrates with popular LMS platforms including Canvas and Blackboard. Grades and feedback can be seamlessly published to your existing systems." + } + } + ] + }; + return ( @@ -237,6 +321,14 @@ export default function RootLayout({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }} /> + + {/* Additional Meta Tags for SEO */} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4d95972..4ca194d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,19 +1,25 @@ "use client"; -import { useState, useEffect, Suspense } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; import Link from "next/link"; import { Eye, EyeOff, ArrowRight, AlertCircle, CheckCircle2, Sparkles } from "lucide-react"; import { AuthProvider, useAuth } from "../context/AuthContext"; function LoginPageContent() { const router = useRouter(); - const searchParams = useSearchParams(); const { login, signup, loginWithGoogle, user, isLoading } = useAuth(); - const [activeTab, setActiveTab] = useState<"login" | "signup">( - searchParams.get("tab") === "signup" ? "signup" : "login" - ); + const [activeTab, setActiveTab] = useState<"login" | "signup">("login"); + + useEffect(() => { const meta = document.createElement("meta"); meta.name = "robots"; meta.content = "noindex"; document.head.appendChild(meta); }, []); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get("tab") === "signup") { + setActiveTab("signup"); + } + }, []); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [fullName, setFullName] = useState(""); @@ -74,18 +80,14 @@ function LoginPageContent() { setLoading(false); }; - if (isLoading) { - return ( - + return ( + + {isLoading ? ( - - ); - } - - return ( - + ) : ( + <> {/* Left: Branding Panel */} @@ -356,6 +358,8 @@ function LoginPageContent() { + > + )} ); } @@ -363,9 +367,7 @@ function LoginPageContent() { export default function LoginPage() { return ( - }> - - + ); } diff --git a/src/app/osm-evaluator/layout.tsx b/src/app/osm-evaluator/layout.tsx new file mode 100644 index 0000000..84801c9 --- /dev/null +++ b/src/app/osm-evaluator/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "OSM (On-Screen Marking) & AI Answer Sheet Evaluator | CBSE ICSE | OzymorLab", + description: "Learn about OSM (On-Screen Marking) for CBSE, ICSE & State Boards. Understand how AI answer sheet evaluators help practice OSM exams. 83% accuracy. 22 languages.", + keywords: [ + "OSM CBSE exam", + "on-screen marking evaluator", + "OSM answer sheet evaluator", + "AI OSM evaluator India", + "on-screen marking checker", + "OSM grading system", + "OSM vs traditional marking", + "on-screen marking vs paper marking", + "how does OSM marking work", + "CBSE on-screen marking", + "OSM marking scheme", + "AI answer sheet evaluation", + "OSM marking reliability", + "digital answer sheet evaluation", + "computer-based marking India", + ], + openGraph: { + title: "OSM (On-Screen Marking) Explained | AI Answer Sheet Evaluator | OzymorLab", + description: "Complete guide to OSM marking for Indian board exams. Learn how AI evaluation helps you practice for CBSE, ICSE, and state board exams.", + type: "article", + locale: "en_IN", + }, + twitter: { + card: "summary_large_image", + title: "OSM Evaluator - Practice with AI for Board Exams", + description: "Understand On-Screen Marking and practice with AI. Get exam-ready for CBSE, ICSE, and state boards.", + }, +}; + +export default function OSMLayout({ children }: { children: React.ReactNode }) { + return <>{children}>; +} diff --git a/src/app/osm-evaluator/page.tsx b/src/app/osm-evaluator/page.tsx index a4b51e1..28e30e9 100644 --- a/src/app/osm-evaluator/page.tsx +++ b/src/app/osm-evaluator/page.tsx @@ -36,7 +36,6 @@ export const metadata: Metadata = { canonical: "https://ozymorlab.vercel.app/osm-evaluator", }, }; - export default function OSMEvaluatorPage() { return ; } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index a2004d1..b4b8441 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "Pricing | AI Essay Grader Pilot Plans | OzymorLab", + description: "Start your institutional pilot for free. Flexible plans for startups, mid-size schools & enterprise districts. 50 free credits included.", + openGraph: { + title: "Pricing | OzymorLab AI Grading Plans", + description: "Free pilot credits, flexible plans for Indian schools of all sizes. Get started today.", + }, +}; + export default function PricingPage() { return ; } diff --git a/src/app/robots.ts b/src/app/robots.ts index 2f829dd..3dbce62 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -23,6 +23,8 @@ export default function robots(): MetadataRoute.Robots { '/admin/*', '/product_admin', '/product_admin/*', + '/analysis', + '/analysis/*', '/api', '/api/*', '/context', diff --git a/test-results/.last-run.json b/test-results/.last-run.json index 957284b..cbcc1fb 100644 --- a/test-results/.last-run.json +++ b/test-results/.last-run.json @@ -1,6 +1,4 @@ { - "status": "failed", - "failedTests": [ - "70b872a5e72a7b2c7282-d4de8269ab84cb7dd8a5" - ] + "status": "passed", + "failedTests": [] } \ No newline at end of file
@@ -486,7 +476,7 @@ export default function LandingPage() { - Empowering your evaluation pipeline + AI-powered evaluation pipeline for modern schools {empowerCards.map((c, i) => ( @@ -503,7 +493,7 @@ export default function LandingPage() { - Elevating standards + AI-grading standards for board exam preparation {elevatingCards.map((c, i) => ( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c056308..29b13e1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -225,6 +225,90 @@ export default function RootLayout({ "areaServed": "IN" }; + const breadcrumbSchema = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": [ + { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://ozymorlab.vercel.app" }, + { "@type": "ListItem", "position": 2, "name": "About", "item": "https://ozymorlab.vercel.app/about" }, + { "@type": "ListItem", "position": 3, "name": "Feature", "item": "https://ozymorlab.vercel.app/feature" }, + { "@type": "ListItem", "position": 4, "name": "Pricing", "item": "https://ozymorlab.vercel.app/pricing" }, + { "@type": "ListItem", "position": 5, "name": "Contact", "item": "https://ozymorlab.vercel.app/contact" }, + { "@type": "ListItem", "position": 6, "name": "Blog", "item": "https://ozymorlab.vercel.app/blog" }, + ] + }; + + const faqSchema = { + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": [ + { + "@type": "Question", + "name": "What is OzymorLab?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab is an AI-powered essay grading platform designed for Indian schools and educational boards. It uses artificial intelligence to automatically evaluate student essays, short answers, and exam responses with per-criterion scores and evidence-backed feedback." + } + }, + { + "@type": "Question", + "name": "How does the AI essay grading work?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Teachers upload answer scripts or student responses, define rubrics or marking schemes, and OzymorLab's AI evaluates each answer against the criteria. It provides per-criterion scores, confidence levels, and specific evidence from the student's response to justify each grade." + } + }, + { + "@type": "Question", + "name": "Which Indian education boards are supported?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab supports CBSE, ICSE, and all major Indian state boards including Maharashtra SSC, UP Board, Rajasthan Board, Tamil Nadu Board, Karnataka SSLC, Kerala Board, West Bengal Board, AP Board, Telangana Board, MP Board, Bihar Board, and NIOS." + } + }, + { + "@type": "Question", + "name": "How many languages does OzymorLab support?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab supports 22 Indian languages including Hindi, Tamil, Telugu, Bengali, Marathi, Gujarati, Kannada, Malayalam, Punjabi, Odia, Urdu, Sanskrit, and English. Students can write answers in their mother tongue and get evaluated in the same language." + } + }, + { + "@type": "Question", + "name": "How accurate is the AI grading?", + "acceptedAnswer": { + "@type": "Answer", + "text": "OzymorLab's AI achieves high accuracy through rubric grounding — aligning AI scores with precise school rubric constraints. Every grade includes a confidence score, and teachers can review, modify, or override any AI-generated grade. The system is designed for explainability, not black-box evaluation." + } + }, + { + "@type": "Question", + "name": "Is there a free plan available?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, new users get 50 free credits to try OzymorLab. Institutional pilots are also available for schools and coaching institutes. Contact us for custom pricing for startups, mid-size schools, and enterprise districts." + } + }, + { + "@type": "Question", + "name": "Does OzymorLab support handwriting recognition?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, OzymorLab includes handwriting OCR support for cursive and printed handwriting, including mathematical equations and scientific diagrams. It transcribes handwritten responses before evaluation." + } + }, + { + "@type": "Question", + "name": "Can OzymorLab integrate with existing school systems?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes, OzymorLab integrates with popular LMS platforms including Canvas and Blackboard. Grades and feedback can be seamlessly published to your existing systems." + } + } + ] + }; + return ( @@ -237,6 +321,14 @@ export default function RootLayout({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }} /> + + {/* Additional Meta Tags for SEO */} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4d95972..4ca194d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,19 +1,25 @@ "use client"; -import { useState, useEffect, Suspense } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; import Link from "next/link"; import { Eye, EyeOff, ArrowRight, AlertCircle, CheckCircle2, Sparkles } from "lucide-react"; import { AuthProvider, useAuth } from "../context/AuthContext"; function LoginPageContent() { const router = useRouter(); - const searchParams = useSearchParams(); const { login, signup, loginWithGoogle, user, isLoading } = useAuth(); - const [activeTab, setActiveTab] = useState<"login" | "signup">( - searchParams.get("tab") === "signup" ? "signup" : "login" - ); + const [activeTab, setActiveTab] = useState<"login" | "signup">("login"); + + useEffect(() => { const meta = document.createElement("meta"); meta.name = "robots"; meta.content = "noindex"; document.head.appendChild(meta); }, []); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get("tab") === "signup") { + setActiveTab("signup"); + } + }, []); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [fullName, setFullName] = useState(""); @@ -74,18 +80,14 @@ function LoginPageContent() { setLoading(false); }; - if (isLoading) { - return ( - + return ( + + {isLoading ? ( - - ); - } - - return ( - + ) : ( + <> {/* Left: Branding Panel */} @@ -356,6 +358,8 @@ function LoginPageContent() { + > + )} ); } @@ -363,9 +367,7 @@ function LoginPageContent() { export default function LoginPage() { return ( - }> - - + ); } diff --git a/src/app/osm-evaluator/layout.tsx b/src/app/osm-evaluator/layout.tsx new file mode 100644 index 0000000..84801c9 --- /dev/null +++ b/src/app/osm-evaluator/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "OSM (On-Screen Marking) & AI Answer Sheet Evaluator | CBSE ICSE | OzymorLab", + description: "Learn about OSM (On-Screen Marking) for CBSE, ICSE & State Boards. Understand how AI answer sheet evaluators help practice OSM exams. 83% accuracy. 22 languages.", + keywords: [ + "OSM CBSE exam", + "on-screen marking evaluator", + "OSM answer sheet evaluator", + "AI OSM evaluator India", + "on-screen marking checker", + "OSM grading system", + "OSM vs traditional marking", + "on-screen marking vs paper marking", + "how does OSM marking work", + "CBSE on-screen marking", + "OSM marking scheme", + "AI answer sheet evaluation", + "OSM marking reliability", + "digital answer sheet evaluation", + "computer-based marking India", + ], + openGraph: { + title: "OSM (On-Screen Marking) Explained | AI Answer Sheet Evaluator | OzymorLab", + description: "Complete guide to OSM marking for Indian board exams. Learn how AI evaluation helps you practice for CBSE, ICSE, and state board exams.", + type: "article", + locale: "en_IN", + }, + twitter: { + card: "summary_large_image", + title: "OSM Evaluator - Practice with AI for Board Exams", + description: "Understand On-Screen Marking and practice with AI. Get exam-ready for CBSE, ICSE, and state boards.", + }, +}; + +export default function OSMLayout({ children }: { children: React.ReactNode }) { + return <>{children}>; +} diff --git a/src/app/osm-evaluator/page.tsx b/src/app/osm-evaluator/page.tsx index a4b51e1..28e30e9 100644 --- a/src/app/osm-evaluator/page.tsx +++ b/src/app/osm-evaluator/page.tsx @@ -36,7 +36,6 @@ export const metadata: Metadata = { canonical: "https://ozymorlab.vercel.app/osm-evaluator", }, }; - export default function OSMEvaluatorPage() { return ; } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index a2004d1..b4b8441 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import LandingPage from "../landing/LandingPage"; +export const metadata: Metadata = { + title: "Pricing | AI Essay Grader Pilot Plans | OzymorLab", + description: "Start your institutional pilot for free. Flexible plans for startups, mid-size schools & enterprise districts. 50 free credits included.", + openGraph: { + title: "Pricing | OzymorLab AI Grading Plans", + description: "Free pilot credits, flexible plans for Indian schools of all sizes. Get started today.", + }, +}; + export default function PricingPage() { return ; } diff --git a/src/app/robots.ts b/src/app/robots.ts index 2f829dd..3dbce62 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -23,6 +23,8 @@ export default function robots(): MetadataRoute.Robots { '/admin/*', '/product_admin', '/product_admin/*', + '/analysis', + '/analysis/*', '/api', '/api/*', '/context', diff --git a/test-results/.last-run.json b/test-results/.last-run.json index 957284b..cbcc1fb 100644 --- a/test-results/.last-run.json +++ b/test-results/.last-run.json @@ -1,6 +1,4 @@ { - "status": "failed", - "failedTests": [ - "70b872a5e72a7b2c7282-d4de8269ab84cb7dd8a5" - ] + "status": "passed", + "failedTests": [] } \ No newline at end of file