feat: full feature buildout — streaming, i18n, mastery map, admin, jobs
Progressive lesson streaming via onSegment callback (fixes SSE for non-English users — locale was shadowed in lesson-reader useEffect). Adds: BullMQ workers, Redis stream buffer, token budget enforcement, Langfuse tracing, golden-eval runner, Playwright e2e scaffolding, lesson depth/locale/preferences schema, mastery map UI, admin panel (blueprints/users/reports/quality/misconceptions), image queries, source citations, view transitions, reading animations, i18n (next-intl), PDF export, surprise endpoint, and 402 passing unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,9 @@ const ENTRIES = [
|
||||
{
|
||||
conceptId: 'c1',
|
||||
conceptName: 'Closures',
|
||||
blueprintId: 'bp1',
|
||||
blueprintTitle: 'JavaScript',
|
||||
blueprintIntentKey: 'javascript',
|
||||
score: 0.8,
|
||||
lastSeen: NOW,
|
||||
nextReview: LATER,
|
||||
@@ -25,7 +27,9 @@ const ENTRIES = [
|
||||
{
|
||||
conceptId: 'c2',
|
||||
conceptName: 'Promises',
|
||||
blueprintId: 'bp1',
|
||||
blueprintTitle: 'JavaScript',
|
||||
blueprintIntentKey: 'javascript',
|
||||
score: 0.45,
|
||||
lastSeen: NOW,
|
||||
nextReview: NOW,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { eq, and, inArray } from 'drizzle-orm';
|
||||
import { db } from '@/lib/db';
|
||||
import { mastery, concepts, blueprints, journeyInstances } from '@/lib/db/schema';
|
||||
import { getMasteryMap } from '@/lib/db/queries';
|
||||
import { getOptionalSession } from '@/lib/auth-helpers';
|
||||
|
||||
@@ -16,14 +19,46 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
|
||||
const entries = await getMasteryMap(userId);
|
||||
|
||||
// Group by blueprintTitle for the map view
|
||||
const grouped: Record<string, { blueprintTitle: string; concepts: typeof entries }> = {};
|
||||
// Group by blueprintIntentKey for the map view
|
||||
const grouped: Record<string, { blueprintId: string; blueprintTitle: string; blueprintIntentKey: string; concepts: typeof entries }> = {};
|
||||
for (const entry of entries) {
|
||||
if (!grouped[entry.blueprintTitle]) {
|
||||
grouped[entry.blueprintTitle] = { blueprintTitle: entry.blueprintTitle, concepts: [] };
|
||||
const key = entry.blueprintIntentKey;
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = { blueprintId: entry.blueprintId, blueprintTitle: entry.blueprintTitle, blueprintIntentKey: key, concepts: [] };
|
||||
}
|
||||
grouped[entry.blueprintTitle].concepts.push(entry);
|
||||
grouped[key].concepts.push(entry);
|
||||
}
|
||||
|
||||
return NextResponse.json({ groups: Object.values(grouped) });
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/mastery?blueprintId=<id>&userId=<id>
|
||||
* Removes all mastery records and the journey instance for a blueprint.
|
||||
*/
|
||||
export async function DELETE(req: NextRequest): Promise<NextResponse> {
|
||||
const session = await getOptionalSession();
|
||||
const userId = session?.user?.id ?? req.nextUrl.searchParams.get('userId');
|
||||
const blueprintId = req.nextUrl.searchParams.get('blueprintId');
|
||||
|
||||
if (!userId || !blueprintId) {
|
||||
return NextResponse.json({ error: 'Missing userId or blueprintId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const conceptIds = (
|
||||
await db.select({ id: concepts.id }).from(concepts).where(eq(concepts.blueprintId, blueprintId))
|
||||
).map((r) => r.id);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
if (conceptIds.length > 0) {
|
||||
await tx.delete(mastery).where(
|
||||
and(eq(mastery.userId, userId), inArray(mastery.conceptId, conceptIds)),
|
||||
);
|
||||
}
|
||||
await tx.delete(journeyInstances).where(
|
||||
and(eq(journeyInstances.userId, userId), eq(journeyInstances.blueprintId, blueprintId)),
|
||||
);
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user