Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
95977ca
initial backend dependencies and database connection
qabalany Feb 23, 2026
7d11b6a
create simple health check route
qabalany Feb 23, 2026
d24e88b
install auth libraries bcryptjs and jsonwebtoken
qabalany Feb 23, 2026
e2b42bd
create user database schema
qabalany Feb 23, 2026
3fb2b72
add password hashing logic to user model
qabalany Feb 23, 2026
6f1d36b
add user registration logic directly to server.js
qabalany Feb 24, 2026
cf101b4
add user login logic directly to server.js
qabalany Feb 24, 2026
01e6fb7
refactor: move auth logic out of server.js into controllers
qabalany Feb 24, 2026
9cca326
refactor: move routes out of server.js into route files
qabalany Feb 24, 2026
3994880
create auth middleware to protect profile routes
qabalany Feb 24, 2026
b0b0744
refactor: extract database connection logic into a config folder
qabalany Feb 24, 2026
635f699
create centralized config file for environment variables
qabalany Feb 24, 2026
a93c5e5
add backend logic for google login verification
qabalany Feb 24, 2026
28d5d70
setup frontend styling with tailwindcss and fonts
qabalany Feb 24, 2026
b8994a3
create axios api client and finalize config
qabalany Feb 24, 2026
1e699bc
write auth service api wrappers
qabalany Feb 24, 2026
51be79a
build global auth context provider
qabalany Feb 24, 2026
3ce2f2b
create database model for logging sessions
qabalany Feb 24, 2026
3cc578f
write dynamic ai persona prompts for Tuwaiq and Ula
qabalany Feb 24, 2026
9a129f5
add openai integration to extract user professions
qabalany Feb 24, 2026
5bd56cc
add liveavatar context creation helper
qabalany Feb 24, 2026
d04fd31
create avatar session start endpoint
qabalany Feb 24, 2026
4365976
create avatar session stop endpoint to log durations
qabalany Feb 24, 2026
ec5854a
create endpoint to interrupt avatar for closing message
qabalany Feb 24, 2026
992b398
add ai transcript analysis post-session endpoint
qabalany Feb 24, 2026
e14474e
create feedback database schemas
qabalany Feb 24, 2026
5bb6503
create feedback submission endpoints
qabalany Feb 24, 2026
7d101af
create generic database seeding script
qabalany Feb 25, 2026
113b1b2
add react-router-dom and build core wrappers
qabalany Feb 25, 2026
4c5cf70
build login page UI
qabalany Feb 25, 2026
4033fd3
build registration page UI
qabalany Feb 25, 2026
7922ab0
add google auth callback handler to App.jsx
qabalany Feb 25, 2026
961f88d
build onboarding header UI
qabalany Feb 25, 2026
3b3b1d8
build mother tongue selection screen
qabalany Feb 25, 2026
226dba2
build target language selection screen
qabalany Mar 2, 2026
a187f05
build avatar character selection screen
qabalany Mar 2, 2026
c9c938d
build profession input screen
qabalany Mar 2, 2026
7e7683b
install livekit client package
qabalany Mar 2, 2026
67ac216
bug fix: onboarding progress bar not advancing
qabalany Mar 2, 2026
c3a093b
build basic avatar session UI skeleton
qabalany Mar 2, 2026
feeeca8
refine avatar session transcript ui
qabalany Mar 2, 2026
58fb1e5
connect user microphone to livekit session
qabalany Mar 2, 2026
62ba2ec
render incoming liveavatar video stream
qabalany Mar 2, 2026
c2790f9
build countdown timer and auto-outro trigger
qabalany Mar 2, 2026
efa8c50
hook up backend session start and stop api calls
qabalany Mar 2, 2026
8d696b4
build ai session review results page
qabalany Mar 2, 2026
7235138
fix video avatar session and timer connection
qabalany Mar 2, 2026
482263c
create the 7-step feedback wizard
qabalany Mar 2, 2026
fe08d34
fix: add live avatar WebRTC datachannel transcript listener
qabalany Mar 2, 2026
7ac2578
fix: deduplicate transcripts and restore UI padding in AvatarSession
qabalany Mar 2, 2026
d731ab6
fix: migrate strict-mode deduplication and DataChannel outro to Avata…
qabalany Mar 2, 2026
70b486d
fix: add bubble padding, re-enable video autoplay muted, fix timer an…
qabalany Mar 2, 2026
46eebbd
fix: render video element continuously and use overlays for connectin…
qabalany Mar 2, 2026
3f9990c
fix(AvatarSession): ensure chat bubbles have padding and prevent scro…
qabalany Mar 2, 2026
60abb83
style: update SessionReview and Feedback pages to Logah light theme
qabalany Mar 2, 2026
8191bd3
fix(branding): use proper favicon and text for Logah logo to avoid vi…
qabalany Mar 2, 2026
75f0292
fix(layout): properly center main container correctly in SessionRevie…
qabalany Mar 2, 2026
f0f881d
style: fix feedback wizard layout, spacing, and tailwind compiler crash
qabalany Mar 2, 2026
f10a8eb
UI: Refine padding and header alignments in Review and Feedback pages
qabalany Mar 2, 2026
0ffb17b
build home dashboard layout and cards
qabalany Mar 3, 2026
b7ca8ff
build user settings UI
qabalany Mar 3, 2026
aefd0b8
fix(UI): restore centered layout constraint for sidebar
qabalany Mar 3, 2026
283652f
build private admin dashboard interface
qabalany Mar 3, 2026
15ca1f8
add tab navigation, sessions panel, and messages panel to admin dashb…
qabalany Mar 3, 2026
98f37bc
add feedback tab with stats cards, filter bar, charts toggle, and fee…
qabalany Mar 3, 2026
c42b800
build accessible sidebar layout component
qabalany Mar 3, 2026
f36b4fe
add analytics aggregations for admin dashboard
qabalany Mar 3, 2026
295bc00
seed demo account on server startup so teachers can test
qabalany Mar 3, 2026
f7f82d6
add bilingual AR/EN support with smooth language toggle
qabalany Mar 3, 2026
f66a894
final accessibility sweep
qabalany Mar 3, 2026
ba4825c
fix responsive layout, circle text overflow, CTA direction, and stale…
qabalany Mar 3, 2026
c09a250
feat: use VITE_API_URL env var for frontend API base URL
qabalany Mar 6, 2026
ce3f71b
fix: set production URLs for API, CORS, and Google OAuth callback
qabalany Mar 6, 2026
9508f88
fix: auto-seed demo and admin users on server startup
qabalany Mar 6, 2026
cf5b21d
add English/Arabic toggle to login & register, set English as default
qabalany Mar 6, 2026
d181cd6
refactor: extract mock data seeding into scripts/seedAll.js
qabalany Mar 6, 2026
6b37722
refactor: use curated feedback data in seedAll, remove seedFeedback.js
qabalany Mar 6, 2026
78c892b
dark mode defaults, demo dashboard button on home page
qabalany Mar 6, 2026
6aa4701
fix: restore missing first feature object in ar translations
qabalany Mar 6, 2026
8e51e91
fix: restore emptied Login, Register, Home pages from disk-full corru…
qabalany Mar 6, 2026
cd29eac
update Home, Login, and Register pages
qabalany Mar 6, 2026
2799a12
unified header across auth and onboarding with dark mode and languag…
qabalany Mar 6, 2026
833cde8
language/dark toggles on review+feedback, white headings & EN transla…
qabalany Mar 6, 2026
cf6947f
full EN translation + consistent RTL/LTR dir on onboarding and sessio…
qabalany Mar 6, 2026
00de0cf
cleanup
qabalany Mar 6, 2026
9b2fb9b
fix: implement Google OAuth server-side flow
qabalany Mar 6, 2026
056504a
fix: surface Google OAuth error reason in login page
qabalany Mar 6, 2026
f466ab9
fix: include onboardingCompleted in login and profile responses
qabalany Mar 6, 2026
c613e6f
fix: restore light mode heading colors and increase flag card opacity
qabalany Mar 6, 2026
6c6caae
fix: translate dashboard charts and fix avgDuration undefined
qabalany Mar 6, 2026
bc1ae0d
fix: full dark mode support for admin dashboard, charts and analytics
qabalany Mar 6, 2026
46241c1
fix: move language and dark mode toggles above CTA in sidebar, blue i…
qabalany Mar 6, 2026
a25af45
fix: close JSX comment in FeedbackCharts causing build error
qabalany Mar 6, 2026
42624c8
fix: accessibility improvements - contrast, heading order, select labels
qabalany Mar 7, 2026
a5e25e3
fix: WCAG
qabalany Mar 7, 2026
12c60e1
fix: accessibility — WCAG 2.1 AA
qabalany Mar 7, 2026
618235d
feat: SessionReview English translation + fix invalid aria-controls o…
qabalany Mar 7, 2026
4a5f0ab
Clean up for submission: remove debug logs, fix manifest, update READ…
qabalany Mar 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
node_modules
.DS_Store
material/
# Environments
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Build directories
build
dist

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

package-lock.json
# Lockfiles (optional, depending on project rules)
package-lock.json

67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,68 @@
# Final Project
# Logah — AI-Powered Language Learning Platform

Replace this readme with your own information about your project.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
Logah is a full-stack language learning web application that connects Arabic-speaking users with AI-powered video avatars for real-time English conversation practice, complete with session analysis, personalised feedback, and progress tracking.

## The problem

Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?
Language learners struggle to find affordable, judgment-free speaking practice. Logah solves this by pairing users with AI avatars (powered by LiveKit + OpenAI) that hold real-time video conversations, adapt dynamically to the user's proficiency level, and deliver post-session grammar and vocabulary feedback.

### Approach & Planning

- Designed the full system in Figma and pitched it early in the course
- Planned using a task board and incremental commits; iterated weekly based on peer feedback
- Used a MERN stack (MongoDB, Express, React, Node.js) with Vite + Tailwind CSS on the frontend
- Integrated LiveKit for WebRTC video streaming and OpenAI for avatar intelligence and session analysis
- Implemented JWT + Passport.js (Local & Google OAuth) for authentication
- Built a bilingual (Arabic/English) interface with full RTL support using React Context
- Applied WCAG 2.1 AA accessibility standards throughout (focus management, ARIA, reduced-motion, keyboard navigation)

### Tech Stack

| Layer | Tools |
|---|---|
| Frontend | React, Vite, Tailwind CSS, React Router, Axios |
| Backend | Node.js, Express, MongoDB, Mongoose |
| Auth | Passport.js (Local + Google OAuth), JWT |
| AI / Video | OpenAI API, LiveKit (WebRTC) |
| Deployment | Azure VM + Coolify (frontend, backend, and MongoDB) |

### What I'd do with more time

- Generate customised exercises targeting each user's specific weaknesses identified from their session history
- Add spaced-repetition vocabulary review between sessions
- Implement native push notifications for daily practice reminders
- Expand avatar personas with more accents and professional domains
- Add a full Lighthouse CI pipeline to maintain the 100 score on every deploy

## View it live

Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.
**Frontend:** https://logah.fartist.live

**Backend API:** https://api.logah.fartist.live/api



## Requirements Checklist

| # | Requirement | Status |
|---|---|---|
| 1 | **Frontend: React** | **PASS** — React 18 with Vite |
| 2 | **Backend: Node.js with Express** | **PASS** — Express 4 |
| 3 | **Database: MongoDB** | **PASS** — Mongoose with 4 models (User, Session, Feedback, AppFeedback) |
| 4 | **Authentication** | **PASS** — JWT + bcrypt + Google OAuth, role-based admin guard |
| 5 | **React Router navigation** | **PASS** — v7, nested layouts, 3 route guard types |
| 6 | **Global state management** | **PASS** — 3 Contexts: Auth, Language (i18n/RTL), Theme (dark mode) |
| 7 | **2+ external libraries** | **PASS** — 13+ (LiveKit, Recharts, Axios, Tailwind, bcryptjs, jsonwebtoken, google-auth-library, etc.) |
| 8 | **Non-curriculum React hook** | **PASS** — `useRef`, `useCallback`, `useMemo` + 3 custom hooks (`useAuth`, `useLanguage`, `useTheme`) |
| 9 | **Chrome, Firefox, Safari support** | **PASS** — standard React/Vite build, no browser-specific code |
| 10 | **Responsive 320px–1600px** | **PASS** — mobile-first Tailwind with `sm:`, `md:`, `lg:`, `xl:` breakpoints everywhere |
| 11 | **Accessibility / Lighthouse 100%** | **PASS** — Skip link, semantic HTML, ARIA roles, focus-visible, reduced-motion, `lang` attribute |
| 12 | **Clean Code** | **PASS** — Clear file structure, consistent naming conventions |
| 13 | **Visual: Box model / margins** | **PASS** — `box-sizing: border-box` reset, consistent spacing |
| 14 | **Visual: h1–h6 typography** | **PASS** — Cairo font, consistent heading styles |
| 15 | **Visual: Color scheme** | **PASS** — CSS custom properties, dark mode support |
| 16 | **Visual: Mobile-optimized** | **PASS** — Mobile-first design throughout |


---

54 changes: 50 additions & 4 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
# Backend part of Final Project
# Logah — Backend API

This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with.
RESTful API server powering the Logah AI language learning platform.

## Tech Stack

- **Node.js** with **Express**
- **MongoDB** with **Mongoose** (4 models: User, Session, Feedback, AppFeedback)
- **JWT** (`jsonwebtoken`) + **bcryptjs** for authentication
- **Google Auth Library** for OAuth 2.0
- **OpenAI API** for session transcript analysis (CEFR grading)
- **LiveAvatar API** for AI avatar video sessions

## API Endpoints

| Method | Route | Description |
|--------|-------|-------------|
| POST | `/api/users/register` | Register a new user |
| POST | `/api/users/login` | Login with email/password |
| POST | `/api/users/google` | Google OAuth login |
| GET | `/api/users/profile` | Get current user profile |
| PATCH | `/api/users/profile` | Update user profile |
| POST | `/api/avatar/create-session` | Create an AI avatar session |
| POST | `/api/avatar/start-session` | Start LiveKit video stream |
| POST | `/api/avatar/stop-session` | Stop session and log to DB |
| POST | `/api/avatar/analyze-session` | AI-powered transcript analysis |
| POST | `/api/feedback` | Submit post-session feedback |
| GET | `/api/feedback` | Get all feedback (admin) |
| POST | `/api/app-feedback` | Submit app bug report/suggestion |
| GET | `/api/app-feedback` | Get all app feedback (admin) |
| GET | `/api/analytics/summary` | Admin analytics summary |

## Getting Started

1. Install the required dependencies using `npm install`.
2. Start the development server using `npm run dev`.
1. Install dependencies: `npm install`
2. Create a `.env` file (see below)
3. Start the dev server: `npm run dev`

### Environment Variables

```
PORT=8080
MONGO_URI=mongodb://localhost:27017/logah
JWT_SECRET=your_jwt_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
CLIENT_URL=http://localhost:5173
OPENAI_API_KEY=your_openai_key
LIVEAVATAR_API_KEY=your_liveavatar_key
```

## Seed Data

The server automatically seeds a demo user and admin user on first startup if the database is empty.
16 changes: 16 additions & 0 deletions backend/config/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import mongoose from "mongoose";
import config from "./index.js";

// Establish connection to the MongoDB database
const connectToDatabase = async () => {
try {
const connection = await mongoose.connect(config.mongoUri);
console.log(`Database connected: ${connection.connection.host}`);
} catch (error) {
console.error(`Database connection failed: ${error.message}`);
// Exit the process if the database connection fails, as the application cannot function without it.
process.exit(1);
}
};

export default connectToDatabase;
25 changes: 25 additions & 0 deletions backend/config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import dotenv from "dotenv";

// Initialize environment variables immediately
dotenv.config();

// Export a single unified config object
const config = {
port: process.env.PORT || 8080,
mongoUri: process.env.MONGO_URI,
jwtSecret: process.env.JWT_SECRET || (() => { console.warn('⚠️ JWT_SECRET not set — using fallback (dev only)'); return 'dev_fallback_secret_change_me'; })(),
googleClientId: process.env.GOOGLE_CLIENT_ID || 'YOUR_GOOGLE_CLIENT_ID',
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
googleCallbackUrl: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:8080/api/users/google/callback',
clientUrl: process.env.CLIENT_URL || 'http://localhost:3000',

// External APIs
liveAvatarApiKey: process.env.LIVEAVATAR_API_KEY,
liveAvatarTuwaiqId: process.env.LIVEAVATAR_AVATAR_ID_TUWAIQ,
liveAvatarUlaId: process.env.LIVEAVATAR_AVATAR_ID_ULA,
liveAvatarTuwaiqVoiceId: process.env.LIVEAVATAR_VOICE_ID_TUWAIQ,
liveAvatarUlaVoiceId: process.env.LIVEAVATAR_VOICE_ID_ULA,
openaiApiKey: process.env.OPENAI_API_KEY,
};

export default config;
127 changes: 127 additions & 0 deletions backend/controllers/analytics.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Session from '../models/Session.model.js';
import User from '../models/User.model.js';

/**
* GET /api/analytics/summary
* Returns aggregate stats for the admin dashboard SessionAnalytics component.
*/
export const getAnalyticsSummary = async (req, res) => {
try {
// ── Totals ────────────────────────────────────────────────────────
const totalSessions = await Session.countDocuments();

const durationAgg = await Session.aggregate([
{ $group: { _id: null, totalSeconds: { $sum: '$durationInSeconds' }, avgSeconds: { $avg: '$durationInSeconds' } } }
]);
const totalSeconds = durationAgg[0]?.totalSeconds || 0;
const avgDurationSeconds = Math.round(durationAgg[0]?.avgSeconds || 0);

// Unique users who have at least one session
const uniqueUserIds = await Session.distinct('user');
const totalUniqueUsers = uniqueUserIds.length;

// ── Sessions per day (last 30 days) ─────────────────────────────
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

const sessionsPerDayRaw = await Session.aggregate([
{ $match: { createdAt: { $gte: thirtyDaysAgo } } },
{
$group: {
_id: {
$dateToString: { format: '%Y-%m-%d', date: '$createdAt' }
},
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } }
]);
const sessionsPerDay = sessionsPerDayRaw.map(d => ({ date: d._id, count: d.count }));

// ── CEFR distribution ────────────────────────────────────────────
const cefrRaw = await Session.aggregate([
{ $match: { cefrLevel: { $exists: true, $ne: null } } },
{ $group: { _id: '$cefrLevel', count: { $sum: 1 } } },
{ $sort: { _id: 1 } }
]);
const cefrDistribution = cefrRaw.map(d => ({ level: d._id, count: d.count }));

// ── Avatar usage ─────────────────────────────────────────────────
const avatarRaw = await Session.aggregate([
{ $group: { _id: '$avatarId', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
const avatarUsage = avatarRaw.map(d => ({ avatar: d._id, count: d.count }));

res.json({
success: true,
data: {
totalSessions,
totalSeconds,
totalMinutes: Math.round(totalSeconds / 60),
totalUniqueUsers,
avgDurationSeconds,
sessionsPerDay,
cefrDistribution,
avatarUsage,
}
});
} catch (error) {
console.error('Analytics summary error:', error);
res.status(500).json({ success: false, message: 'Failed to fetch analytics summary' });
}
};

/**
* GET /api/analytics/user-activity
* Returns per-user session stats for the admin activity table.
*/
export const getUserActivity = async (req, res) => {
try {
const activityRaw = await Session.aggregate([
{
$group: {
_id: '$user',
sessionCount: { $sum: 1 },
totalSeconds: { $sum: '$durationInSeconds' },
lastSession: { $max: '$createdAt' },
// Collect all CEFR levels to find the most common one
cefrLevels: { $push: '$cefrLevel' }
}
},
{ $sort: { lastSession: -1 } },
{ $limit: 50 },
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'userInfo'
}
},
{ $unwind: { path: '$userInfo', preserveNullAndEmptyArrays: true } }
]);

const activity = activityRaw.map(row => {
// Find the most-common CEFR level for this user
const levelCounts = {};
(row.cefrLevels || []).forEach(l => { if (l) levelCounts[l] = (levelCounts[l] || 0) + 1; });
const topLevel = Object.entries(levelCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || '—';

return {
userId: row._id,
name: row.userInfo?.name || 'مستخدم',
email: row.userInfo?.email || '',
sessionCount: row.sessionCount,
totalSeconds: row.totalSeconds,
lastSession: row.lastSession,
cefrLevel: topLevel,
};
});

res.json({ success: true, data: activity });
} catch (error) {
console.error('User activity error:', error);
res.status(500).json({ success: false, message: 'Failed to fetch user activity' });
}
};
42 changes: 42 additions & 0 deletions backend/controllers/appFeedback.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppFeedback from '../models/AppFeedback.model.js';

// This endpoint allows any user (logged in or guest) to submit a bug report
// or general comment about the application through the global header icon.
export const submitAppFeedback = async (req, res) => {
try {
const { name, message, userId } = req.body;

const newFeedback = await AppFeedback.create({
name,
message,
// If the user isn't logged in, userId is undefined, so null is passed to the database
userId: userId || null
});

res.status(201).json({
success: true,
data: newFeedback,
message: 'Feedback submitted successfully'
});
} catch (error) {
console.error('Error saving app feedback:', error);
res.status(500).json({ success: false, message: 'Failed to save feedback', details: error.message });
}
};

// This endpoint powers the admin dashboard's bug report table.
// It returns a list of all bug reports, starting with the most recent.
export const getAllAppFeedback = async (req, res) => {
try {
const feedbacks = await AppFeedback.find().sort({ createdAt: -1 });

res.status(200).json({
success: true,
count: feedbacks.length,
data: feedbacks
});
} catch (error) {
console.error('Error fetching app feedbacks:', error);
res.status(500).json({ success: false, message: 'Failed to fetch bug reports', details: error.message });
}
};
Loading