Skip to content

Commit 10f8295

Browse files
committed
Apply reviewer suggestions for PR #159
Made-with: Cursor
1 parent 7c26491 commit 10f8295

8 files changed

Lines changed: 65 additions & 43 deletions

File tree

app/(coach)/dashboard/DashboardClient.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import {
3838
getStudyStreak,
3939
getWeeklyAttemptStats,
4040
} from '../../services/studentOS';
41-
import type { QuestionAttempt } from '../../services/studentOS/types';
4241
import { generateDashboardInsights } from '../../services/AICoachService';
4342

4443
interface AIInsights {
@@ -172,7 +171,7 @@ export default function DashboardClient({
172171
}, [subjectStats]);
173172

174173
const topWeaknesses = useMemo(() => {
175-
const counts = weaknessCountsFromAttempts(attempts as QuestionAttempt[]);
174+
const counts = weaknessCountsFromAttempts(attempts);
176175
return pickTopWeaknesses(counts, 6);
177176
}, [attempts]);
178177

app/api/gemini/route.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server';
22
import { createClient } from '@/app/lib/supabase/server';
33

44
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || "";
5+
// ~3.2M base64 chars ≈ ~2.4MB binary — stay under typical 4.5MB request limits (e.g. Vercel).
6+
const MAX_INLINE_BASE64_CHARS = 3_200_000;
57

68
export async function POST(request) {
79
try {
@@ -79,8 +81,6 @@ export async function POST(request) {
7981
console.warn('Could not extract mime type');
8082
continue;
8183
}
82-
// ~3.2M base64 chars ≈ ~2.4MB binary — stay under typical 4.5MB request limits (e.g. Vercel).
83-
const MAX_INLINE_BASE64_CHARS = 3_200_000;
8484
if (data.length > MAX_INLINE_BASE64_CHARS) {
8585
return NextResponse.json(
8686
{

app/api/hackclub/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function POST(request: Request) {
1818
);
1919
}
2020

21-
const { success } = await checkRateLimit(user.id, 10);
21+
const { success } = await checkRateLimit(user.id, 10, 'hackclub');
2222
if (!success) {
2323
return NextResponse.json({ error: "Rate limit exceeded. Please wait a minute." }, { status: 429 });
2424
}

app/api/openrouter/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export async function POST(request: Request) {
6868
);
6969
}
7070

71-
const { success } = await checkRateLimit(user.id, 10);
71+
const { success } = await checkRateLimit(user.id, 10, 'openrouter');
7272
if (!success) {
7373
return NextResponse.json({ error: "Rate limit exceeded. Please wait a minute." }, { status: 429 });
7474
}

app/components/UIComponents.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ import DOMPurify from 'dompurify';
55
import { Upload, CheckCircle, Brain, ChevronRight, RefreshCw, Sparkles, Send, RotateCcw, X, Check } from 'lucide-react';
66
import katex from 'katex';
77

8+
let dompurifyAnchorRelHookRegistered = false;
9+
function ensureDompurifyAnchorRelHook() {
10+
if (dompurifyAnchorRelHookRegistered) return;
11+
DOMPurify.addHook('afterSanitizeAttributes', (node: Element) => {
12+
if (node.tagName === 'A') {
13+
node.setAttribute('rel', 'noopener noreferrer');
14+
}
15+
});
16+
dompurifyAnchorRelHookRegistered = true;
17+
}
18+
819
interface MarkdownTextProps {
920
text: string;
1021
className?: string;
@@ -247,7 +258,7 @@ const renderMarkdown = (rawText: string): string => {
247258
// ⚡ Bolt: Hoisted DOMPurify configuration object outside the component body.
248259
// This prevents recreating this large configuration object on every useMemo computation
249260
// when rendering MarkdownText, reducing memory allocation overhead and GC churn.
250-
const DOMPURIFY_CONFIG = {
261+
const DOMPURIFY_CONFIG: import('dompurify').Config = {
251262
ADD_TAGS: ['math', 'mrow', 'annotation', 'semantics', 'mtext', 'mn', 'mo', 'mi', 'mspace', 'mover', 'munder', 'munderover', 'mfrac', 'msqrt', 'mroot', 'merror', 'mpadded', 'mphantom', 'menclose', 'ms', 'mglyph', 'maligngroup', 'malignmark', 'mtable', 'mtr', 'mtd', 'svg', 'path', 'line', 'circle', 'rect', 'polygon', 'polyline', 'ellipse', 'g', 'defs', 'clippath', 'use'],
252263
ADD_ATTR: ['aria-hidden', 'focusable', 'role', 'd', 'viewBox', 'fill', 'stroke', 'stroke-width', 'x', 'y', 'width', 'height', 'xmlns', 'xlink:href'],
253264
ALLOWED_TAGS: [
@@ -267,16 +278,12 @@ const DOMPURIFY_CONFIG = {
267278
'ul'
268279
],
269280
ALLOWED_ATTR: ['class', 'style'],
270-
afterSanitizeAttributes(node: Element) {
271-
if (node.tagName === 'A') {
272-
node.setAttribute('rel', 'noopener noreferrer');
273-
}
274-
}
275-
} as import('dompurify').Config;
281+
};
276282

277283
export const MarkdownText = memo(({ text, className = "" }: MarkdownTextProps) => {
278284
const sanitizedHTML = useMemo(() => {
279285
if (!text) return "";
286+
ensureDompurifyAnchorRelHook();
280287
const rendered = renderMarkdown(text);
281288
return DOMPurify.sanitize(rendered, DOMPURIFY_CONFIG);
282289
}, [text]);

app/lib/rateLimit.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,26 @@ const upstashToken = process.env.UPSTASH_REDIS_REST_TOKEN;
1616
const upstashRedis =
1717
upstashUrl && upstashToken ? new Redis({ url: upstashUrl, token: upstashToken }) : null;
1818

19-
const upstashLimiters = new Map<number, Ratelimit>();
19+
if (upstashRedis) {
20+
console.info('[rateLimit] Upstash Redis rate limiting enabled');
21+
} else {
22+
console.info('[rateLimit] Using in-memory rate limiting (no Upstash config)');
23+
}
24+
25+
const upstashLimiters = new Map<string, Ratelimit>();
2026

21-
function getUpstashLimiter(limit: number): Ratelimit | null {
27+
function getUpstashLimiter(namespace: string, limit: number): Ratelimit | null {
2228
if (!upstashRedis) return null;
23-
let lim = upstashLimiters.get(limit);
29+
const key = `${namespace}:${limit}`;
30+
let lim = upstashLimiters.get(key);
2431
if (!lim) {
32+
const safeNs = namespace.replace(/[^a-zA-Z0-9_-]/g, '_');
2533
lim = new Ratelimit({
2634
redis: upstashRedis,
2735
limiter: Ratelimit.slidingWindow(limit, '60 s'),
28-
prefix: 'aimarker-rl',
36+
prefix: `aimarker-rl:${safeNs}`,
2937
});
30-
upstashLimiters.set(limit, lim);
38+
upstashLimiters.set(key, lim);
3139
}
3240
return lim;
3341
}
@@ -80,12 +88,14 @@ function checkRateLimitInMemory(identifier: string, limit: number): { success: b
8088
*/
8189
export async function checkRateLimit(
8290
identifier: string,
83-
limit: number = 10
91+
limit: number = 10,
92+
namespace: string = 'global'
8493
): Promise<{ success: boolean; limit: number; remaining: number }> {
85-
const lim = getUpstashLimiter(limit);
94+
const lim = getUpstashLimiter(namespace, limit);
8695
if (lim) {
8796
const res = await lim.limit(identifier);
8897
return { success: res.success, limit, remaining: res.remaining };
8998
}
90-
return checkRateLimitInMemory(identifier, limit);
99+
const namespacedId = `${namespace}:${identifier}`;
100+
return checkRateLimitInMemory(namespacedId, limit);
91101
}

app/services/studentOS/attempts.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export async function logQuestionAttemptSafe(row: Partial<QuestionAttempt>, clie
4343
}
4444
}
4545

46-
export function weaknessCountsFromAttempts(attempts: QuestionAttempt[]): Record<string, number> {
46+
export function weaknessCountsFromAttempts(
47+
attempts: { primary_flaw?: string | null }[]
48+
): Record<string, number> {
4749
const counts: Record<string, number> = {};
4850
for (const a of attempts || []) {
4951
const key = (a.primary_flaw || '').trim();
@@ -116,7 +118,6 @@ export async function getTopicPerformance(studentId: string, client?: StudentOSS
116118
.select('subject_id, marks_awarded, marks_total, primary_flaw, question_type')
117119
.eq('student_id', studentId)
118120
.order('attempted_at', { ascending: false })
119-
.limit(500)
120121
.returns<
121122
{
122123
subject_id: string | null;
@@ -179,7 +180,6 @@ export async function getSubjectPerformance(studentId: string, client?: StudentO
179180
.select('subject_id, marks_awarded, marks_total')
180181
.eq('student_id', studentId)
181182
.order('attempted_at', { ascending: false })
182-
.limit(500)
183183
.returns<{ subject_id: string | null; marks_awarded: number | null; marks_total: number | null }[]>();
184184

185185
if (error || !attempts?.length) return {};

app/services/studentOS/sessions.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { supabase } from '../supabaseClient';
21
import { isoToday } from '../dateUtils';
32
import { StudySession } from './types';
43
import { clientOrDefault, type StudentOSSupabase } from './getSupabase';
@@ -47,9 +46,10 @@ export async function getOrCreateTodayDailySession(
4746
return inserted;
4847
}
4948

50-
export async function completeSession(studentId: string, sessionId: string, reflection: string): Promise<StudySession> {
49+
export async function completeSession(studentId: string, sessionId: string, reflection: string, client?: StudentOSSupabase): Promise<StudySession> {
5150
if (!studentId) throw new Error('studentId required');
52-
const { data, error } = await (supabase.from('study_sessions') as any)
51+
const db = clientOrDefault(client);
52+
const { data, error } = await (db.from('study_sessions') as any)
5353
.update({
5454
status: 'done',
5555
reflection,
@@ -64,10 +64,10 @@ export async function completeSession(studentId: string, sessionId: string, refl
6464
return data;
6565
}
6666

67-
export async function listSessions(studentId: string, { fromDateISO, toDateISO }: { fromDateISO?: string; toDateISO?: string }): Promise<StudySession[]> {
67+
export async function listSessions(studentId: string, { fromDateISO, toDateISO }: { fromDateISO?: string; toDateISO?: string } = {}, client?: StudentOSSupabase): Promise<StudySession[]> {
6868
if (!studentId) throw new Error('studentId required');
69-
70-
let q = supabase.from('study_sessions').select('*').eq('student_id', studentId).order('planned_for', { ascending: true });
69+
const db = clientOrDefault(client);
70+
let q = db.from('study_sessions').select('*').eq('student_id', studentId).order('planned_for', { ascending: true });
7171

7272
if (fromDateISO) q = q.gte('planned_for', fromDateISO);
7373
if (toDateISO) q = q.lte('planned_for', toDateISO);
@@ -80,8 +80,9 @@ export async function listSessions(studentId: string, { fromDateISO, toDateISO }
8080
/**
8181
* Create a new study session
8282
*/
83-
export async function createSession(studentId: string, sessionData: Partial<StudySession>): Promise<StudySession> {
83+
export async function createSession(studentId: string, sessionData: Partial<StudySession>, client?: StudentOSSupabase): Promise<StudySession> {
8484
if (!studentId) throw new Error('studentId required');
85+
const db = clientOrDefault(client);
8586
const payload = {
8687
student_id: studentId,
8788
subject_id: sessionData.subject_id || null,
@@ -94,17 +95,18 @@ export async function createSession(studentId: string, sessionData: Partial<Stud
9495
topic: sessionData.topic || null,
9596
start_time: sessionData.start_time || null,
9697
};
97-
const { data, error } = await supabase.from('study_sessions').insert(payload as any).select('*').single();
98+
const { data, error } = await db.from('study_sessions').insert(payload as any).select('*').single();
9899
if (error) throw error;
99100
return data;
100101
}
101102

102103
/**
103104
* Update an existing study session
104105
*/
105-
export async function updateSession(studentId: string, sessionId: string, patch: Partial<StudySession>): Promise<StudySession> {
106+
export async function updateSession(studentId: string, sessionId: string, patch: Partial<StudySession>, client?: StudentOSSupabase): Promise<StudySession> {
106107
if (!studentId) throw new Error('studentId required');
107-
const { data, error } = await (supabase.from('study_sessions') as any)
108+
const db = clientOrDefault(client);
109+
const { data, error } = await (db.from('study_sessions') as any)
108110
.update({ ...patch, updated_at: new Date().toISOString() })
109111
.eq('student_id', studentId)
110112
.eq('id', sessionId)
@@ -117,9 +119,10 @@ export async function updateSession(studentId: string, sessionId: string, patch:
117119
/**
118120
* Delete a study session
119121
*/
120-
export async function deleteSession(studentId: string, sessionId: string): Promise<void> {
122+
export async function deleteSession(studentId: string, sessionId: string, client?: StudentOSSupabase): Promise<void> {
121123
if (!studentId) throw new Error('studentId required');
122-
const { error } = await supabase
124+
const db = clientOrDefault(client);
125+
const { error } = await db
123126
.from('study_sessions')
124127
.delete()
125128
.eq('student_id', studentId)
@@ -131,11 +134,12 @@ export async function deleteSession(studentId: string, sessionId: string): Promi
131134
* Batch save sessions from AI-generated schedule
132135
* Clears existing planned sessions for the week and inserts new ones
133136
*/
134-
export async function saveSchedule(studentId: string, sessions: Partial<StudySession>[], weekStartISO: string, weekEndISO: string): Promise<StudySession[]> {
137+
export async function saveSchedule(studentId: string, sessions: Partial<StudySession>[], weekStartISO: string, weekEndISO: string, client?: StudentOSSupabase): Promise<StudySession[]> {
135138
if (!studentId) throw new Error('studentId required');
139+
const db = clientOrDefault(client);
136140

137141
// Delete existing planned sessions for this week (except completed ones)
138-
const { error: deleteError } = await supabase
142+
const { error: deleteError } = await db
139143
.from('study_sessions')
140144
.delete()
141145
.eq('student_id', studentId)
@@ -161,7 +165,7 @@ export async function saveSchedule(studentId: string, sessions: Partial<StudySes
161165

162166
if (payloads.length === 0) return [];
163167

164-
const { data, error } = await supabase.from('study_sessions').insert(payloads as any).select('*');
168+
const { data, error } = await db.from('study_sessions').insert(payloads as any).select('*');
165169
if (error) throw error;
166170
return data || [];
167171
}
@@ -236,15 +240,16 @@ export async function getStudyStreak(studentId: string, client?: StudentOSSupaba
236240
* Get recent study history (last 14 days) to avoid repetition in AI scheduling
237241
* Returns topics that were recently studied so AI can schedule spaced repetition
238242
*/
239-
export async function getRecentStudyHistory(studentId: string, days = 14) {
243+
export async function getRecentStudyHistory(studentId: string, days = 14, client?: StudentOSSupabase) {
240244
if (!studentId) return { recentTopics: [], completedSessions: [], skippedCount: 0 };
241245

242246
try {
247+
const db = clientOrDefault(client);
243248
const startDate = new Date();
244249
startDate.setDate(startDate.getDate() - days);
245250
const startISO = startDate.toISOString().split('T')[0];
246251

247-
const { data: sessions, error } = await supabase
252+
const { data: sessions, error } = await db
248253
.from('study_sessions')
249254
.select('id, subject_id, topic, status, planned_for, duration_minutes, session_type')
250255
.eq('student_id', studentId)
@@ -293,15 +298,16 @@ export async function getRecentStudyHistory(studentId: string, days = 14) {
293298
* Get session completion stats for AI feedback loop
294299
* Tracks patterns in when sessions are completed vs skipped
295300
*/
296-
export async function getSessionCompletionStats(studentId: string) {
301+
export async function getSessionCompletionStats(studentId: string, client?: StudentOSSupabase) {
297302
if (!studentId) return { completionRate: 0, byDayOfWeek: {}, byTimeOfDay: {}, insights: [] };
298303

299304
try {
305+
const db = clientOrDefault(client);
300306
const thirtyDaysAgo = new Date();
301307
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
302308
const startISO = thirtyDaysAgo.toISOString().split('T')[0];
303309

304-
const { data: sessions, error } = await supabase
310+
const { data: sessions, error } = await db
305311
.from('study_sessions')
306312
.select('id, status, planned_for, session_type, duration_minutes, start_time')
307313
.eq('student_id', studentId)

0 commit comments

Comments
 (0)