Skip to content

Commit 5d58d8a

Browse files
apartsinclaude
andcommitted
Remove mockup screens, add documentation, accessibility fixes, and test cleanup
- Remove old mockup screen gallery (screens/) — real system in code/client/ - Deprecate root stub server — redirect root npm scripts to code/ - Add README, developer guide, instructor guide, student guide - Add 6 pedagogical session logs with real LLM interactions - Add aria-label, aria-live, role=alert across all client pages - Add test-cleanup endpoint to auto-remove e2e test artifacts - Add e2e afterAll cleanup hook to prevent DB pollution - Clean up test-generated courses/challenges/users from dev DB - Fix dotenv override, real LLM assertions in integration tests - Update i18n content and institution data across all languages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d3aff32 commit 5d58d8a

88 files changed

Lines changed: 16578 additions & 15433 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

code/README.md

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# AI CoReasoning Lab
2+
3+
A co-reasoning assessment platform where students develop critical thinking skills by collaborating with AI. Students frame ill-defined problems, judge AI-generated solutions, and steer the AI toward better outputs -- all while receiving LLM-evaluated feedback.
4+
5+
## Key Features
6+
7+
- **Three-Phase Challenge Runs** -- Framing, Judging, and Steering phases that assess co-reasoning skills
8+
- **Multiple Response Types** -- Multiple choice (MC) or open-ended responses per phase
9+
- **LLM-Powered Evaluation** -- Automated grading and feedback via OpenAI, Groq, or other providers
10+
- **Practice & Assessment Modes** -- Practice shows feedback after each phase; assessment reveals results only at completion
11+
- **Multilingual Support** -- English, Hebrew, French, German, and Spanish UI with separate content language for LLM prompts
12+
- **Role-Based Access** -- Student, Instructor, and Admin roles with appropriate permissions
13+
- **Course Management** -- Hierarchical subject trees, course subscriptions, instructor assignments
14+
- **Analytics & Reporting** -- Per-student and per-course analytics with PDF export
15+
- **Bulk Import** -- YAML-based import of institutions, users, courses, and challenges
16+
17+
## Tech Stack
18+
19+
| Layer | Technology |
20+
|-------|-----------|
21+
| **Runtime** | Node.js >= 20 |
22+
| **Framework** | Express 4.x |
23+
| **Database** | SQLite (dev/test) / PostgreSQL 16 (staging/prod) |
24+
| **ORM** | Knex.js 3.x |
25+
| **Auth** | Passport.js (local email/password + Google OAuth 2.0) |
26+
| **LLM** | OpenAI SDK (gpt-4o-mini default, gpt-4o full) + Groq (llama-3.3-70b fallback) |
27+
| **Validation** | Zod |
28+
| **Sessions** | express-session (memory store for SQLite, connect-pg-simple for PostgreSQL) |
29+
| **PDF** | PDFKit |
30+
| **Logging** | Winston |
31+
| **Security** | Helmet, CORS, bcryptjs |
32+
| **Tests** | Jest + Supertest (unit/integration), Playwright + Chromium (e2e) |
33+
34+
## Project Structure
35+
36+
```
37+
code/
38+
client/ # Static frontend (HTML pages, JS, CSS)
39+
js/ # Client-side JavaScript modules
40+
content-compiled/ # Compiled i18n bundles (en.js, he.js, etc.)
41+
styles.css # Application stylesheet
42+
*.html # Page templates (SPA-style, served statically)
43+
server/
44+
index.js # Express app entry point
45+
routes/ # API route handlers
46+
services/ # Business logic layer
47+
middleware/ # Auth, validation, error handling, logging
48+
db/
49+
knexfile.js # Database configuration
50+
migrations/ # Schema migrations
51+
seeds/ # Seed data
52+
llm/
53+
prompt-engine.js # YAML prompt template loader and renderer
54+
utils/ # Constants, errors, helpers, logger, tracing
55+
prompts/ # LLM prompt templates (YAML)
56+
content/ # i18n content source files (YAML)
57+
en/ # English UI labels, tooltips, scenarios
58+
he/ # Hebrew
59+
fr/ de/ es/ # French, German, Spanish
60+
import/
61+
sample/ # Sample import YAML files
62+
tests/
63+
unit/ # Jest unit tests
64+
integration/ # Jest + Supertest API tests
65+
e2e/ # Playwright browser tests
66+
setup.js # Test environment setup
67+
docker/ # Docker and docker-compose files
68+
render.yaml # Render.com deployment blueprint
69+
```
70+
71+
## Getting Started
72+
73+
### Prerequisites
74+
75+
- Node.js >= 20.0.0
76+
- npm
77+
- (Optional) PostgreSQL 16 for production-like setup
78+
- (Optional) Docker and Docker Compose
79+
80+
### Installation
81+
82+
```bash
83+
cd code
84+
npm install
85+
```
86+
87+
### Environment Variables
88+
89+
Create a `.env.all` file in the `code/` directory:
90+
91+
```env
92+
# Required
93+
SESSION_SECRET=your-random-secret-string
94+
95+
# LLM Providers (at least one recommended)
96+
OPENAI_API_KEY=sk-...
97+
GROQ_API_KEY=gsk_...
98+
99+
# Optional: Override default models
100+
OPENAI_MODEL=gpt-4o-mini
101+
OPENAI_MODEL_FULL=gpt-4o
102+
GROQ_MODEL=llama-3.3-70b-versatile
103+
104+
# Optional: Google OAuth
105+
GOOGLE_OAUTH_CLIENT_ID=...
106+
GOOGLE_OAUTH_CLIENT_SECRET=...
107+
108+
# Optional: PostgreSQL (defaults to SQLite for development)
109+
DATABASE_URL=postgres://user:password@localhost:5432/coreason
110+
111+
# Optional: Other LLM providers
112+
GEMINI_API_KEY=...
113+
COHERE_API_KEY=...
114+
```
115+
116+
If no LLM API keys are set, the application runs with fallback placeholder responses for all LLM features.
117+
118+
### Database Setup
119+
120+
```bash
121+
# Run migrations and seed data
122+
npm run db:setup
123+
124+
# Or step by step:
125+
npm run db:migrate
126+
npm run db:seed
127+
```
128+
129+
### Build Content
130+
131+
Compile i18n YAML content into JavaScript bundles:
132+
133+
```bash
134+
npm run build
135+
```
136+
137+
### Run the Server
138+
139+
```bash
140+
# Development (with auto-reload)
141+
npm run dev
142+
143+
# Production
144+
npm start
145+
```
146+
147+
The server starts on `http://localhost:3000` by default.
148+
149+
### Import Sample Data
150+
151+
```bash
152+
npm run import -- import/sample/full-import.yaml
153+
```
154+
155+
This populates the database with demo institutions, users, courses (with subject trees), and challenges in both English and Hebrew.
156+
157+
## Running Tests
158+
159+
```bash
160+
# All tests
161+
npm run test:all
162+
163+
# Unit tests only
164+
npm run test:unit
165+
166+
# Integration tests (API tests with supertest)
167+
npm run test:integration
168+
169+
# End-to-end tests (requires running server)
170+
npm run test:e2e
171+
```
172+
173+
### Test Configuration
174+
175+
- **Unit tests**: Jest with Node environment, coverage thresholds at 80% lines/functions/statements and 70% branches
176+
- **Integration tests**: Jest + Supertest against in-memory SQLite
177+
- **E2E tests**: Playwright with Chromium, auto-starts dev server on port 3000
178+
179+
### Linting & Formatting
180+
181+
```bash
182+
npm run lint
183+
npm run lint:fix
184+
npm run format
185+
```
186+
187+
## Docker
188+
189+
```bash
190+
# Development (SQLite, auto-reload)
191+
npm run docker:dev
192+
193+
# Production (PostgreSQL)
194+
npm run docker:prod
195+
```
196+
197+
The production Docker setup uses a multi-stage build (Node 20 Alpine) with `tini` as the init process and PostgreSQL 16 Alpine as the database.
198+
199+
## Deployment
200+
201+
### Render.com
202+
203+
The project includes a `render.yaml` blueprint for one-click deployment to Render:
204+
205+
- **Web service**: Free plan, Oregon region, Node runtime
206+
- **Database**: PostgreSQL (free plan)
207+
- **Build**: `npm install && npm run build && npm run db:setup`
208+
- **Start**: `node server/index.js`
209+
210+
Required environment variables on Render:
211+
- `OPENAI_API_KEY` or `GROQ_API_KEY` (for LLM features)
212+
- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET` (for Google login)
213+
- `SESSION_SECRET` (auto-generated by Render)
214+
- `DATABASE_URL` (auto-linked from Render PostgreSQL)
215+
216+
## API Overview
217+
218+
All API endpoints are prefixed with `/api/v1/`. See the [Developer Guide](docs/developer-guide.md) for the complete API reference.
219+
220+
| Route Group | Base Path | Description |
221+
|------------|-----------|-------------|
222+
| Auth | `/api/v1/auth` | Register, login, logout, Google OAuth, test-login |
223+
| Users | `/api/v1/users` | Profile management, stats |
224+
| Institutions | `/api/v1/institutions` | List and retrieve institutions |
225+
| Courses | `/api/v1/courses` | CRUD, subscriptions, subject trees, LLM generation |
226+
| Challenges | `/api/v1/challenges` | CRUD, publish, archive, preview |
227+
| Runs | `/api/v1/runs` | Start runs, submit phases, complete, reports |
228+
| Analytics | `/api/v1/analytics` | Student and instructor analytics, PDF export |
229+
| Import | `/api/v1/import` | YAML batch import |
230+
| LLM | `/api/v1/llm` | Preview generation, model listing |
231+
| Admin | `/api/v1/admin` | System overview, user/course/challenge management |
232+
| Health | `/api/health` | Health check (database + LLM status) |
233+
234+
## License
235+
236+
Proprietary. All rights reserved.

code/client/add-course.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
<span style="background:#e0f2f1;color:#00695c;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px" data-t="instructor">Instructor</span>
6565
<span id="userInstitution" style="font-size:12px;color:#1a237e;background:#e8eaf6;padding:2px 10px;border-radius:12px;font-weight:600;"></span>
6666
<span id="userName">&mdash;</span>
67-
<a href="profile.html"><div class="avatar" id="userAvatar" style="background:#e3f2fd;color:var(--primary);">?</div></a>
67+
<a href="profile.html" aria-label="User profile"><div class="avatar" id="userAvatar" style="background:#e3f2fd;color:var(--primary);">?</div></a>
6868
<a href="#" class="btn-logout" onclick="API.auth.logout().then(()=>window.location.href='/login.html').catch(()=>window.location.href='/login.html');return false;" style="margin-left:8px;background:var(--primary);border:none;border-radius:var(--radius);padding:5px 14px;font-size:12px;color:#fff;cursor:pointer;text-decoration:none;font-weight:600;" data-t="logout">Logout</a>
6969
</div>
7070
</header>
@@ -83,30 +83,30 @@ <h1>Add New Course</h1>
8383
<h3 style="margin-bottom:16px;">Course Details</h3>
8484

8585
<div class="form-group">
86-
<label>Course Name <span style="color:var(--danger);">*</span></label>
86+
<label for="courseName">Course Name <span style="color:var(--danger);">*</span></label>
8787
<input type="text" class="form-control" id="courseName" placeholder="e.g., Introduction to Machine Learning" oninput="updatePreview()">
8888
</div>
8989

9090
<div class="form-group">
91-
<label>Course Code</label>
91+
<label for="courseCode">Course Code</label>
9292
<input type="text" class="form-control" id="courseCode" placeholder="e.g., CS-401" oninput="updatePreview()">
9393
</div>
9494

9595
<div class="form-group">
96-
<label data-t="department">Department <span style="color:var(--danger);">*</span></label>
96+
<label for="courseDept" data-t="department">Department <span style="color:var(--danger);">*</span></label>
9797
<select class="form-control" id="courseDept" onchange="updatePreview()">
9898
<option value="" data-t="selectDepartment">Select department...</option>
9999
</select>
100100
</div>
101101

102102
<div class="form-group">
103-
<label data-t="institution">Institution</label>
103+
<label for="courseInstitution" data-t="institution">Institution</label>
104104
<input type="text" class="form-control" id="courseInstitution" value="" disabled style="opacity:0.7;">
105105
<p class="text-sm text-muted mt-4" data-t="institutionScopedNote">Courses are scoped to your institution and cannot be changed</p>
106106
</div>
107107

108108
<div class="form-group">
109-
<label>Description</label>
109+
<label for="courseDesc">Description</label>
110110
<textarea class="form-control" rows="3" id="courseDesc" placeholder="Brief description of the course content and objectives..." oninput="updatePreview()"></textarea>
111111
</div>
112112

code/client/challenge-list-instructor.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<span style="background:#e0f2f1;color:#00695c;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px" data-t="instructor">Instructor</span>
4545
<span id="userInstitution" style="font-size:12px;color:#1a237e;background:#e8eaf6;padding:2px 10px;border-radius:12px;font-weight:600;"></span>
4646
<span id="userName"></span>
47-
<a href="profile.html"><div class="avatar" id="userAvatar" style="background:#e3f2fd;color:var(--primary);">?</div></a>
47+
<a href="profile.html" aria-label="User profile"><div class="avatar" id="userAvatar" style="background:#e3f2fd;color:var(--primary);">?</div></a>
4848
<a href="#" class="btn-logout" onclick="API.auth.logout().then(()=>window.location.href='/login.html').catch(()=>window.location.href='/login.html');return false;" style="margin-left:8px;background:var(--primary);border:none;border-radius:var(--radius);padding:5px 14px;font-size:12px;color:#fff;cursor:pointer;text-decoration:none;font-weight:600;" data-t="logout">Logout</a>
4949
</div>
5050
</header>
@@ -238,12 +238,12 @@ <h1 data-t="myChallenges">My Challenges</h1>
238238
// Actions
239239
var tdActions = document.createElement('td');
240240
var actHtml = '<div class="challenge-actions">';
241-
actHtml += '<a href="create-challenge.html?id=' + id + '" class="btn btn-outline btn-sm" data-t="edit">Edit</a>';
241+
actHtml += '<a href="create-challenge.html?id=' + id + '" class="btn btn-outline btn-sm" data-t="edit" aria-label="Edit">Edit</a>';
242242
if (chStatus === 'published') {
243243
actHtml += ' <a href="instructor-analytics.html?courseId=' + (ch.course_id || ch.courseId || '') + '" class="btn btn-outline btn-sm" data-t="analytics">Analytics</a>';
244244
}
245245
actHtml += ' <a href="challenge-run.html?challengeId=' + id + '&preview=1" class="btn btn-outline btn-sm" data-t="preview">Preview</a>';
246-
actHtml += ' <button class="btn btn-outline btn-sm" style="color:var(--danger);" onclick="deleteChallenge(\'' + id + '\')" title="Delete">&#128465;</button>';
246+
actHtml += ' <button class="btn btn-outline btn-sm" style="color:var(--danger);" onclick="deleteChallenge(\'' + id + '\')" title="Delete" aria-label="Delete">&#128465;</button>';
247247
actHtml += '</div>';
248248
tdActions.innerHTML = actHtml;
249249
tr.appendChild(tdActions);

code/client/challenge-list.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<span style="background:#e8eaf6;color:#1a237e;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px" data-t="student">Student</span>
3333
<span id="userInstitution" style="font-size:12px;color:#1a237e;background:#e8eaf6;padding:2px 10px;border-radius:12px;font-weight:600;"></span>
3434
<span id="userName"></span>
35-
<a href="profile.html"><div class="avatar" id="userAvatar">?</div></a>
35+
<a href="profile.html" aria-label="User profile"><div class="avatar" id="userAvatar">?</div></a>
3636
<a href="#" class="btn-logout" onclick="API.auth.logout().then(()=>window.location.href='/login.html').catch(()=>window.location.href='/login.html');return false;" style="margin-left:8px;background:var(--primary);border:none;border-radius:var(--radius);padding:5px 14px;font-size:12px;color:#fff;cursor:pointer;text-decoration:none;font-weight:600;" data-t="logout">Logout</a>
3737
</div>
3838
</header>
@@ -263,8 +263,8 @@ <h1 data-t="challenges">Challenges</h1>
263263

264264
// Private challenges: owner gets edit & delete
265265
if (vis === 'private' && isOwner) {
266-
html += ' <a href="create-challenge-student.html?id=' + id + '" class="btn btn-outline btn-sm" title="Edit">&#9998;</a>';
267-
html += ' <button class="btn btn-outline btn-sm" title="Delete" style="color:var(--danger);" onclick="deleteChallenge(\'' + id + '\')">&#128465;</button>';
266+
html += ' <a href="create-challenge-student.html?id=' + id + '" class="btn btn-outline btn-sm" title="Edit" aria-label="Edit">&#9998;</a>';
267+
html += ' <button class="btn btn-outline btn-sm" title="Delete" style="color:var(--danger);" onclick="deleteChallenge(\'' + id + '\')" aria-label="Delete">&#128465;</button>';
268268
}
269269

270270
html += '</div>';

code/client/challenge-report.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
<span style="background:#e8eaf6;color:#1a237e;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px" data-t="student">Student</span>
133133
<span id="userInstitution" style="font-size:12px;color:#1a237e;background:#e8eaf6;padding:2px 10px;border-radius:12px;font-weight:600;"></span>
134134
<span id="userName">&mdash;</span>
135-
<a href="profile.html"><div class="avatar" id="userAvatar">?</div></a>
135+
<a href="profile.html" aria-label="User profile"><div class="avatar" id="userAvatar">?</div></a>
136136
<a href="#" class="btn-logout" onclick="API.auth.logout().then(()=>window.location.href='/login.html').catch(()=>window.location.href='/login.html');return false;" style="margin-left:8px;background:var(--primary);border:none;border-radius:var(--radius);padding:5px 14px;font-size:12px;color:#fff;cursor:pointer;text-decoration:none;font-weight:600;" data-t="logout">Logout</a>
137137
</div>
138138
</header>

code/client/challenge-run.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,14 @@
140140
<div class="user-area">
141141
<select class="lang-select"><option value="en">EN</option><option value="he">HE</option><option value="fr">FR</option><option value="de">DE</option><option value="es">ES</option></select>
142142
<span style="background:#e8eaf6;color:#1a237e;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px" data-t="student">Student</span>
143-
<span id="userName">Student</span>
144-
<a href="profile.html"><div class="avatar" id="userAvatar">S</div></a>
143+
<span id="userName">&mdash;</span>
144+
<a href="profile.html" aria-label="User profile"><div class="avatar" id="userAvatar">?</div></a>
145145
<a href="#" class="btn-logout" onclick="API.auth.logout().then(()=>window.location.href='/login.html').catch(()=>window.location.href='/login.html');return false;" style="margin-left:8px;background:var(--primary);border:none;border-radius:var(--radius);padding:5px 14px;font-size:12px;color:#fff;cursor:pointer;text-decoration:none;font-weight:600;">Logout</a>
146146
</div>
147147
</header>
148148

149149
<main class="page">
150-
<div id="scenario-content">
150+
<div id="scenario-content" aria-live="polite">
151151
<div class="card" style="text-align:center;padding:40px">
152152
<div class="llm-spinner" style="width:32px;height:32px;border-width:3px;margin:0 auto 16px;border-color:rgba(26,35,126,.2);border-top-color:var(--primary)"></div>
153153
<p class="text-muted" data-t="loading">Loading...</p>
@@ -156,13 +156,13 @@
156156
</main>
157157

158158
<!-- LLM Status -->
159-
<div class="llm-status" id="llm-status">
159+
<div class="llm-status" id="llm-status" aria-live="polite">
160160
<div class="llm-spinner"></div>
161161
<span id="llm-status-text" data-t="generating">Generating...</span>
162162
</div>
163163

164164
<!-- Error Toast -->
165-
<div class="error-toast" id="error-toast" onclick="this.classList.remove('visible')">
165+
<div class="error-toast" id="error-toast" role="alert" onclick="this.classList.remove('visible')">
166166
<span id="error-toast-text" data-t="error">Error</span>
167167
</div>
168168

code/client/content-loader.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ function switchLanguage(lang) {
121121
// Persist preference to localStorage
122122
try { localStorage.setItem(LANG_STORAGE_KEY, lang); } catch(e) {}
123123

124+
// Sync preferred_language to server (fire-and-forget; won't block UI)
125+
if (typeof API !== 'undefined' && API.put) {
126+
API.put('/users/me', { preferred_language: lang }).catch(function() {
127+
// Ignore — user may not be logged in yet
128+
});
129+
}
130+
124131
// Auto-translate the page
125132
translatePage();
126133

code/client/course-catalog-instructor.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<span style="background:#e0f2f1;color:#00695c;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px" data-t="instructor">Instructor</span>
4747
<span id="userInstitution" style="font-size:12px;color:#1a237e;background:#e8eaf6;padding:2px 10px;border-radius:12px;font-weight:600;"></span>
4848
<span id="userName">&mdash;</span>
49-
<a href="profile.html"><div class="avatar" id="userAvatar" style="background:#e3f2fd;color:var(--primary);">?</div></a>
49+
<a href="profile.html" aria-label="User profile"><div class="avatar" id="userAvatar" style="background:#e3f2fd;color:var(--primary);">?</div></a>
5050
<a href="#" class="btn-logout" onclick="API.auth.logout().then(()=>window.location.href='/login.html').catch(()=>window.location.href='/login.html');return false;" style="margin-left:8px;background:var(--primary);border:none;border-radius:var(--radius);padding:5px 14px;font-size:12px;color:#fff;cursor:pointer;text-decoration:none;font-weight:600;" data-t="logout">Logout</a>
5151
</div>
5252
</header>

0 commit comments

Comments
 (0)