Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ coverage/
!.env.example

.npm-cache/
.vercel
pnpm-lock.yaml
note.js

backend/
20260403070000_add_mastery_and_rooms.sql


note.js


package-lock.json
77 changes: 43 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,50 @@
# Yantra

Yantra is a Next.js 16 prototype for an AI-native learning platform. The live build currently combines:
Yantra is a Next.js 16 prototype for an AI-native learning platform. The current build combines:

- a public marketing site
- Supabase-backed email/password authentication
- Supabase-backed Google sign-in
- a public docs/help center with article pages
- protected learner routes
- a persisted student profile
- a public learner dashboard
- a locally persisted student profile
- a public editor with local project storage
- a public docs/help center
- optional Supabase-backed auth surfaces (email/password, Google, GitHub, password reset)
- persisted access requests
- authenticated chat history continuity
- a configurable Python Yantra AI microservice under `ai/`, targeted first by the main chat and Python Room feedback routes
- a room-only Sarvam-powered voice assistant layered onto the Python Room through Next.js server routes
- authenticated chat history continuity when Supabase auth is available
- a Render-backed Yantra chat assistant routed through the Python AI microservice when configured
- a room-only Sarvam-powered voice assistant layered onto the Python Room
- a separate Python AI microservice under `ai/`

The app is no longer just a static marketing-plus-dashboard shell. Authentication, profile persistence, docs/help content, chat continuity, and the first real Python Room are live. Most learner-state, recommendation, and broader room logic are still seeded or presentation-led.
The app is no longer just a static marketing shell. The public dashboard, student profile, editor, docs, and live Python room are real. Some learner-state, recommendation, and deeper roadmap logic are still seeded or presentation-led.

## Current Routes

### Public pages

- `/` marketing landing page
- `/login` email/password sign-in
- `/signup` account creation
- `/dashboard` student dashboard
- `/dashboard/student-profile` editable student profile workspace
- `/dashboard/rooms/python` Python room index
- `/dashboard/rooms/python/control-flow-calibration` live Python room
- `/editor` public editor workspace
- `/editor/projects` local project list
- `/editor/share/[shareSlug]` shared project view
- `/docs` docs home
- `/docs/[slug]` docs article pages
- `/features`
- `/guide`
- `/login`
- `/signup`
- `/privacy`
- `/terms`
- `/status`
- `/auth/reset-password` password recovery completion
- `/reset-password` mirror of the reset-password experience

### Auth-required pages

- `/onboarding` onboarding flow for authenticated accounts
- `/dashboard` student dashboard
- `/dashboard/student-profile` editable student profile workspace
- `/dashboard/rooms/python` Python Room
- `/auth/reset-password`
- `/reset-password`

### Auth handlers
### Additional app routes

- `/auth/confirm` email confirmation and Google OAuth callback
- `/auth/signout` sign-out route
- `/onboarding`
- `/auth/confirm`
- `/auth/signout`

### API routes

Expand All @@ -54,32 +58,37 @@ The app is no longer just a static marketing-plus-dashboard shell. Authenticatio
- `POST /api/rooms/python/feedback`
- `POST /api/sarvam/stt`
- `POST /api/sarvam/tts`
- `GET /api/editor/projects`
- `POST /api/editor/projects`
- `GET /api/editor/projects/[projectId]`
- `PATCH /api/editor/projects/[projectId]`
- `PUT /api/editor/projects/[projectId]/files`
- `POST /api/editor/projects/[projectId]/share`
- `GET /api/editor/share/[shareSlug]`

## What Works Today

- Next.js App Router runtime on a Vercel-compatible setup
- Supabase SSR auth with cookie refresh handled through `proxy.ts`
- automatic profile seeding in `public.profiles` for first-time signed-in users
- onboarding shown after new account creation, with returning login going to the dashboard
- public dashboard access with no required sign-in
- profile persistence from `/dashboard/student-profile`
- auth-required dashboard and Python Room redirects for signed-out visitors
- password reset email flow and reset page
- Google OAuth sign-in through Supabase
- local editor projects and shared-project remix flows
- Supabase-backed auth pages and recovery flows
- reusable chat widget on the marketing site and dashboard
- main Yantra chat routed through the Python AI service target resolved by `src/lib/yantra-ai-service.ts`
- docs-only Support Desk answers powered by Gemini through `/api/docs-support`
- authenticated chat history restore across sessions
- authenticated chat history restore across sessions when Supabase auth is available
- Python Room execution in-browser through Pyodide, with runtime-error line highlighting
- Python Room feedback through `/api/rooms/python/feedback`, which targets the Python AI service first and can fall back to Gemini
- room-only push-to-talk voice assistant using Sarvam STT/TTS plus `/api/chat`
- automated coverage for the room-feedback route, Pyodide error parsing, and the Python service test suite
- access-request form validation, persistence, and server handling

## What Is Still Seeded Or Limited

- dashboard skills, progress cards, curriculum nodes, and recommendations are still starter data
- most room cards outside the Python Room are still presentation-led
- some room cards and roadmap visuals outside the live Python room remain presentation-led
- Python Room correctness checking is still exception-only; successful-but-wrong output is not evaluated yet
- chat moderation, analytics, and tool use are not built yet
- cross-device sync for public-mode profiles and editor projects is still future work
- adaptive roadmap logic and deeper learner memory are still future work

## Project Structure
Expand Down Expand Up @@ -169,7 +178,7 @@ Notes:

### Database setup

Run the SQL in `supabase/schema.sql` against your Supabase project. It creates:
Run the SQL in `supabase/schema.sql` against your Supabase project. That creates `public.profiles`, `public.access_requests`, `public.chat_histories`, plus the update triggers and row-level security policies used by access requests and any Supabase-backed legacy profile/chat persistence that is still enabled.

- `public.profiles`
- `public.access_requests`
Expand Down
2 changes: 1 addition & 1 deletion app/api/chat/history/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function GET() {
const result = await getAuthenticatedChatHistory();

if (!result) {
return NextResponse.json({ error: 'Unauthorized.' }, { status: 401 });
return NextResponse.json({ messages: [] });
}

return NextResponse.json({ messages: result.messages });
Expand Down
3 changes: 3 additions & 0 deletions app/api/editor/assist/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const runtime = 'nodejs';

export { POST } from '@/editor/api/assist';
1 change: 1 addition & 0 deletions app/api/editor/projects/[projectId]/files/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PUT } from '@/editor/api/project-files';
1 change: 1 addition & 0 deletions app/api/editor/projects/[projectId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET, PATCH } from '@/editor/api/project';
1 change: 1 addition & 0 deletions app/api/editor/projects/[projectId]/share/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { POST } from '@/editor/api/project-share';
1 change: 1 addition & 0 deletions app/api/editor/projects/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET, POST } from '@/editor/api/projects';
1 change: 1 addition & 0 deletions app/api/editor/share/[shareSlug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET } from '@/editor/api/shared-project';
42 changes: 2 additions & 40 deletions app/auth/confirm/route.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,5 @@
import { redirect } from 'next/navigation';
import { type EmailOtpType } from '@supabase/supabase-js';
import { hasSupabaseEnv } from '@/src/lib/supabase/env';
import { createClient } from '@/src/lib/supabase/server';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const tokenHash = searchParams.get('token_hash');
const next = searchParams.get('next') || '/onboarding';
const type = searchParams.get('type') as EmailOtpType | null;

if (!hasSupabaseEnv()) {
redirect('/login?message=Configure%20Supabase%20first.&kind=error');
}

const supabase = await createClient();

if (code) {
const { error } = await supabase.auth.exchangeCodeForSession(code);

if (error) {
redirect('/login?message=We%20could%20not%20finish%20that%20sign-in.%20Please%20try%20again.&kind=error');
}

redirect(next);
}

if (!tokenHash || !type) {
redirect('/login?message=The%20confirmation%20link%20is%20invalid.&kind=error');
}

const { error } = await supabase.auth.verifyOtp({
type,
token_hash: tokenHash,
});

if (error) {
redirect('/login?message=We%20could%20not%20verify%20that%20email%20link.%20Please%20try%20again.&kind=error');
}

redirect(next);
export async function GET() {
redirect('/dashboard');
}
5 changes: 2 additions & 3 deletions app/auth/reset-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ResetPasswordExperience from '@/src/features/auth/ResetPasswordExperience';
import { hasSupabaseEnv } from '@/src/lib/supabase/env';
import { redirect } from 'next/navigation';

export default function ResetPasswordPage() {
return <ResetPasswordExperience supabaseConfigured={hasSupabaseEnv()} />;
redirect('/dashboard');
}
9 changes: 1 addition & 8 deletions app/auth/signout/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { redirect } from 'next/navigation';
import { hasSupabaseEnv } from '@/src/lib/supabase/env';
import { createClient } from '@/src/lib/supabase/server';

export async function GET() {
if (hasSupabaseEnv()) {
const supabase = await createClient();
await supabase.auth.signOut();
}

redirect('/login?message=Secure%20session%20closed.%20Return%20when%20you%20are%20ready%20to%20continue.&kind=info');
redirect('/dashboard');
}
18 changes: 3 additions & 15 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import StudentDashboard from '@/src/features/dashboard/StudentDashboard';
import { requireAuthenticatedProfile } from '@/src/lib/supabase/route-guards';
import { getAuthenticatedDashboardData } from '@/src/lib/supabase/dashboard';
import PublicStudentDashboard from '@/src/features/dashboard/PublicStudentDashboard';

export default async function DashboardPage() {
const profileResult = await requireAuthenticatedProfile({
unauthenticatedRedirect: '/login?message=Log%20in%20to%20open%20your%20dashboard.&kind=info',
});

const result = await getAuthenticatedDashboardData(profileResult);

if (!result) {
return null;
}

return <StudentDashboard data={result} />;
export default function DashboardPage() {
return <PublicStudentDashboard />;
}
15 changes: 10 additions & 5 deletions app/dashboard/rooms/python/control-flow-calibration/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { notFound } from 'next/navigation';
import PythonRoomShell from '@/src/features/rooms/PythonRoomShell';
import { requireAuthenticatedProfile } from '@/src/lib/supabase/route-guards';
import { fetchPythonRoomById } from '@/src/lib/supabase/python-rooms';

const CONTROL_FLOW_CALIBRATION_ROOM_ID = 'control-flow-calibration';

export default async function DashboardPythonControlFlowCalibrationPage() {
await requireAuthenticatedProfile({
unauthenticatedRedirect: '/login?message=Log%20in%20to%20open%20the%20Python%20Room.&kind=info',
});
const room = await fetchPythonRoomById(CONTROL_FLOW_CALIBRATION_ROOM_ID);

if (!room) {
notFound();
}

return <PythonRoomShell />;
return <PythonRoomShell room={room} />;
}
22 changes: 4 additions & 18 deletions app/dashboard/rooms/python/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
import { requireAuthenticatedProfile } from '@/src/lib/supabase/route-guards';
import { fetchPythonRooms } from '@/src/lib/supabase/python-rooms';
import { redirect } from 'next/navigation';
import { defaultStudentProfile } from '@/src/features/dashboard/student-profile-model';
import PythonRoomsIndexPage from '@/src/features/rooms/PythonRoomsIndexPage';

export default async function DashboardPythonRoomsPageIndex() {
await requireAuthenticatedProfile({
unauthenticatedRedirect: '/login?message=Log%20in%20to%20open%20the%20Python%20Room.&kind=info',
});

const rooms = await fetchPythonRooms();
if (rooms && rooms.length > 0) {
redirect(`/dashboard/rooms/python/${rooms[0].id}`);
}

return (
<div className="flex h-screen items-center justify-center bg-black text-white">
No rooms available
</div>
);
export default function DashboardPythonRoomsPageIndex() {
return <PythonRoomsIndexPage learnerName={defaultStudentProfile.name} />;
}
17 changes: 7 additions & 10 deletions app/dashboard/student-profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { requireAuthenticatedProfile } from '@/src/lib/supabase/route-guards';
import StudentProfileOverview from '@/src/features/dashboard/StudentProfileOverview';

export default async function StudentProfileIndexPage() {
const result = await requireAuthenticatedProfile({
unauthenticatedRedirect: '/login?message=Log%20in%20to%20open%20your%20profile.&kind=info',
});
import StudentProfilePage from '@/src/features/dashboard/StudentProfilePage';
import { defaultStudentProfile } from '@/src/features/dashboard/student-profile-model';

export default function DashboardStudentProfilePage() {
return (
<StudentProfileOverview
initialProfileData={result.profile}
defaultProfileData={result.defaultProfile}
<StudentProfilePage
initialProfileData={defaultStudentProfile}
defaultProfileData={defaultStudentProfile}
publicMode
/>
);
}
15 changes: 12 additions & 3 deletions app/docs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import DocsArticlePage from '@/src/features/docs/DocsArticlePage';
import { docsArticles, getDocsArticleBySlug } from '@/src/features/docs/docs-content';
import {
getDocsArticleBySlug,
getVisibleDocsArticles,
isHiddenDocsArticleSlug,
} from '@/src/features/docs/docs-content';

type DocsArticleRouteProps = {
params: Promise<{
Expand All @@ -10,7 +14,7 @@ type DocsArticleRouteProps = {
};

export async function generateStaticParams() {
return docsArticles.map((article) => ({
return getVisibleDocsArticles().map((article) => ({
slug: article.slug,
}));
}
Expand All @@ -33,6 +37,11 @@ export async function generateMetadata({ params }: DocsArticleRouteProps): Promi

export default async function DocsArticleRoute({ params }: DocsArticleRouteProps) {
const { slug } = await params;

if (isHiddenDocsArticleSlug(slug)) {
redirect('/docs');
}

const article = getDocsArticleBySlug(slug);

if (!article) {
Expand Down
2 changes: 1 addition & 1 deletion app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import DocsHomePage from '@/src/features/docs/DocsHomePage';

export const metadata: Metadata = {
title: 'Yantra Docs',
description: 'Guides, onboarding help, account support, and product explanations for Yantra.',
description: 'Guides for the public dashboard, local profile, editor, and product explanations for Yantra.',
};

export default function DocsPage() {
Expand Down
14 changes: 1 addition & 13 deletions app/editor/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1 @@
export default function EditorPage() {
return (
<main
className="flex min-h-screen items-center justify-center"
style={{
background: '#1e1e1e',
color: '#d4d4d4',
}}
>
<p className="text-2xl font-semibold tracking-tight">working on</p>
</main>
);
}
export { default } from '@/editor/editor-pages/EditorPage';
1 change: 1 addition & 0 deletions app/editor/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/editor/editor-pages/ProjectsPage';
1 change: 1 addition & 0 deletions app/editor/share/[shareSlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/editor/editor-pages/SharedProjectPage';
Loading
Loading