Make it stand out.

INTRODUCE YOUR BRAND

# Macroeconomics Virtual Tutor – MVP Embed Pack This package gives you a working, iframe‑embeddable tutor focused on AP Macroeconomics (Money Market + AD/AS). It’s designed for quick deployment on Vercel with Supabase. --- ## 0) Project structure (Next.js App Router) ``` macro-tutor/ ├─ app/ │ ├─ embed/ # iframe-safe UI │ │ └─ page.tsx │ ├─ api/ │ │ ├─ start-session/route.ts │ │ ├─ next/route.ts │ │ └─ grade/route.ts │ └─ layout.tsx ├─ components/ │ ├─ TutorPanel.tsx │ └─ GraphAD_AS.tsx ├─ lib/ │ ├─ db.ts # Supabase client (server) │ └─ pedagogy.ts # Heath-model flow helpers ├─ public/ │ └─ logo.svg ├─ package.json ├─ tsconfig.json ├─ .env.local.example └─ README.md ``` --- ## 1) Environment & installation **.env.local** (copy from example) ``` NEXT_PUBLIC_SUPABASE_URL=your-project-url SUPABASE_SERVICE_ROLE_KEY=your-service-role NEXT_PUBLIC_APP_NAME="Heath Macro Tutor" ``` Install & run ``` pnpm i pnpm dev ``` Deploy to Vercel; add the same env vars there. --- ## 2) Supabase: schema (SQL) Run this in Supabase SQL editor. ```sql -- USERS & CLASSES create table if not exists users ( id uuid primary key default gen_random_uuid(), role text check (role in ('student','teacher')) default 'student', display_name text, created_at timestamptz default now() ); create table if not exists classes ( id uuid primary key default gen_random_uuid(), teacher_id uuid references users(id) on delete cascade, name text not null, period text, created_at timestamptz default now() ); create table if not exists enrollments ( class_id uuid references classes(id) on delete cascade, user_id uuid references users(id) on delete cascade, primary key (class_id, user_id) ); -- CONTENT create table if not exists standards ( id bigserial primary key, framework text not null, code text not null, label text not null ); create table if not exists strategies ( id bigserial primary key, name text not null, description text, tags text[] ); create table if not exists items ( id bigserial primary key, unit text not null, -- e.g., 'MoneyMarket', 'AD_AS' type text check (type in ('MC','FRQ')) not null, stem text not null, choices jsonb, -- for MC answer jsonb, -- canonical answer explanation text, -- why correct; include distractor rationales tags text[] -- misconceptions, standards refs ); -- SESSIONS & ATTEMPTS create table if not exists sessions ( id uuid primary key default gen_random_uuid(), user_id uuid, class_id uuid, started_at timestamptz default now(), meta jsonb ); create table if not exists messages ( id bigserial primary key, session_id uuid references sessions(id) on delete cascade, role text check (role in ('student','tutor')) not null, text text, data jsonb, created_at timestamptz default now() ); create table if not exists attempts ( id bigserial primary key, session_id uuid references sessions(id) on delete cascade, item_id bigint references items(id) on delete cascade, response jsonb, correct boolean, feedback text, elapsed_ms integer, created_at timestamptz default now() ); -- GRAPH STATES (for simple SVG widgets) create table if not exists graphs ( id bigserial primary key, session_id uuid references sessions(id) on delete cascade, type text, -- 'AD_AS', 'MoneyMarket' state jsonb, created_at timestamptz default now() ); -- RLS (simple, anonymous-friendly) alter table sessions enable row level security; alter table attempts enable row level security; alter table messages enable row level security; alter table graphs enable row level security; -- Anonymous can only see their own session data if a session token matches meta->>'anon_key' create policy anon_read_session on sessions for select using (true); create policy anon_insert_session on sessions for insert with check (true); create policy anon_insert_attempt on attempts for insert with check (true); create policy anon_insert_message on messages for insert with check (true); create policy anon_insert_graph on graphs for insert with check (true); ``` ### Seed: core items (Money Market → AD) ```sql insert into items (unit, type, stem, choices, answer, explanation, tags) values ('MoneyMarket','MC', 'If the central bank sells bonds in the open market, what happens in the money market and aggregate demand in the short run?', '{"A":"Ms ↓ ⇒ i ↑ ⇒ I ↓ ⇒ AD shifts left","B":"Ms ↑ ⇒ i ↓ ⇒ I ↑ ⇒ AD shifts right","C":"G ↑ so AD shifts right","D":"No change to AD in short run"}', '{"correct":"A"}', 'Open market **sale** drains reserves → money supply falls, nominal interest rate rises, investment falls → AD left (PL ↓, Y ↓). B reverses the operation; C confuses monetary with fiscal; D ignores i→I channel.', '{"OMOs","investment channel","AD shift"} ), ('AD_AS','MC', 'An economy experiences a negative supply shock. In the AD–AS model, what is the immediate effect?', '{"A":"SRAS left ⇒ PL ↑, Y ↓","B":"AD left ⇒ PL ↓, Y ↓","C":"LRAS right ⇒ PL ↓, Y ↑","D":"SRAS right ⇒ PL ↓, Y ↑"}', '{"correct":"A"}', 'Negative supply shock (e.g., oil price spike) raises per-unit costs → SRAS left → stagflationary outcome (higher PL, lower Y).', '{"supply shock","stagflation"} ); ``` --- ## 3) Next.js: minimal pages & APIs ### app/layout.tsx ```tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ### app/embed/page.tsx (iframe-safe UI) ```tsx 'use client'; import { useEffect, useState } from 'react'; import TutorPanel from '@/components/TutorPanel'; export default function Embed() { const [sessionId, setSessionId] = useState(null); const [unit, setUnit] = useState('MoneyMarket'); useEffect(() => { const url = new URL(window.location.href); const u = url.searchParams.get('unit'); if (u) setUnit(u); (async () => { const res = await fetch('/api/start-session', { method: 'POST' }); const json = await res.json(); setSessionId(json.session_id); })(); }, []); return (

Heath Macro Tutor

Unit: {unit} • Modeled → Guided → You Try → Check → Why Others Are Wrong

{sessionId && } {!sessionId &&

Starting session…

}
); } ``` ### components/TutorPanel.tsx ```tsx 'use client'; import { useEffect, useState } from 'react'; type Item = { id:number; type:'MC'|'FRQ'; stem:string; choices?:Record }; type Step = 'purpose'|'microdiag'|'model'|'guided'|'independent'|'retrieval'|'exit'; export default function TutorPanel({ sessionId, unit }:{sessionId:string; unit:string}){ const [step, setStep] = useState('purpose'); const [item, setItem] = useState(); const [choice, setChoice] = useState(''); const [feedback, setFeedback] = useState(''); useEffect(()=>{ nextStep(); },[]); async function nextStep(last?:any){ const res = await fetch('/api/next', { method: 'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify({ session_id: sessionId, unit, last }) }); const json = await res.json(); setStep(json.step); setItem(json.item); setFeedback(''); setChoice(''); } async function grade(){ if(!item) return; const res = await fetch('/api/grade', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ session_id: sessionId, item_id: item.id, response: {choice} }) }); const json = await res.json(); setFeedback(json.feedback); } return (
{step==='purpose' && (

Purpose (10s)

We’ll connect **monetary policy** in the Money Market to **AD shifts** in the AD–AS model. Goal: predict PL and Y.

)} {item && (

{item.type==='MC' ? 'Check for Understanding' : 'FRQ'}

{item.stem}

{item.type==='MC' && item.choices && (
{Object.entries(item.choices).map(([k,v])=> ( ))}
)}
{feedback &&

{feedback}

}
)}
); } ``` ### app/api/start-session/route.ts ```ts import { NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { randomUUID } from 'crypto'; export async function POST(){ // Anonymous session id (store in cookie for continuity) const sessionId = randomUUID(); cookies().set('session_id', sessionId, { httpOnly: true, sameSite:'lax' }); return NextResponse.json({ session_id: sessionId }); } ``` ### app/api/next/route.ts ```ts import { NextResponse } from 'next/server'; import { sql } from '@vercel/postgres'; // or use Supabase JS if preferred export async function POST(req: Request){ const { unit } = await req.json(); // Fetch one MC item for the unit; rotate later const { rows } = await sql`select id, type, stem, choices from items where unit = ${unit} limit 1`; const item = rows?.[0] || null; const step = 'independent'; return NextResponse.json({ step, item }); } ``` ### app/api/grade/route.ts ```ts import { NextResponse } from 'next/server'; import { sql } from '@vercel/postgres'; export async function POST(req: Request){ const { item_id, response } = await req.json(); const { rows } = await sql`select answer, explanation from items where id=${item_id}`; if(!rows?.length) return NextResponse.json({ correct:false, feedback:'No item found.' }); const ans = rows[0].answer?.correct; const correct = response?.choice === ans; const why = rows[0].explanation as string; const feedback = correct ? `Correct! ${why}` : `Not yet. ${why}`; return NextResponse.json({ correct, feedback }); } ``` > Note: swap `@vercel/postgres` for Supabase JS if you prefer; both work. For Supabase, use a server client with the service role key to read items. --- ## 4) Simple AD–AS graph widget (optional) ### components/GraphAD_AS.tsx (sketch) ```tsx 'use client'; import { useState } from 'react'; export default function GraphADAS(){ const [adShift, setAdShift] = useState(0); // -1 left, 1 right return (
{/* SRAS */} {/* AD base + shift */}
); } ``` --- ## 5) Squarespace embed code (copy/paste) Add a **Code Block** on your Squarespace page and paste: ```html
``` To switch units: ```html ``` --- ## 6) Teacher‑facing auto‑lesson (export stub) Return a JSON/markdown payload the teacher can paste into your **Common Lesson Plan Template**. Example server response shape: ```json { "standards": ["CCSS.ELA-LITERACY.RST.11-12.1", "C3.D2.Eco.1.9-12"], "objectives": [ "Explain how open market operations affect the money supply and nominal interest rates.", "Predict short-run changes in PL and Y when monetary policy shifts AD." ], "lesson_flow": ["Purpose (1m)", "Model (2m)", "Guided (3m)", "Independent (3m)", "Exit Ticket (1m)"], "exit_ticket": "If the central bank purchases bonds, what happens to i, I, AD, PL, and Y?" } ``` --- ## 7) Roadmap toggles * Add **diagnostic pathing** (micro-quiz → choose model/guided/challenge). * Implement **spaced_queue** table & cron job (Supabase Scheduled Functions). * Build **teacher dashboard** at `/teacher` (auth-gated) with CSV export of attempts. --- ## 8) Notes on privacy & FERPA * Keep anonymous sessions by default in embeds (no PII). Rostered classes only on teacher dashboard. * Enable RLS on any table that stores session artifacts; gate teacher views by user id. --- ## 9) Quick seed expansion (paste into SQL to add more MC) ```sql insert into items (unit, type, stem, choices, answer, explanation, tags) values ('MoneyMarket','MC', 'If required reserve ratio increases with no other changes, what happens to the money supply and interest rates?', '{"A":"Ms ↑, i ↓","B":"Ms ↓, i ↑","C":"Ms ↔, i ↔","D":"Ms ↓, i ↓"}', '{"correct":"B"}', 'Higher rr lowers the money multiplier → money supply contracts → nominal interest rate tends to rise.', '{"rr","multiplier"} ), ('AD_AS','MC', 'Government raises spending while the central bank holds money supply constant. Short-run effect?', '{"A":"AD right; PL ↑, Y ↑","B":"SRAS right; PL ↓, Y ↑","C":"AD left; PL ↓, Y ↓","D":"LRAS left; PL ↑, Y ↓"}', '{"correct":"A"}', 'Expansionary fiscal shifts AD right in short run when Ms constant; typical outcome: higher output and price level.', '{"fiscal","AD"} ); ``` --- ## 10) Minimal package.json (excerpt) ```json { "name": "macro-tutor", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { "next": "14.2.4", "react": "18.3.1", "react-dom": "18.3.1", "@vercel/postgres": "^0.9.0", "typescript": "^5.6.2" } } ``` --- ## 11) Teacher UX copy (in‑app) * **Purpose:** “Today we’ll connect money policy → interest rate → investment → AD → PL & Y.” * **Hint tokens:** “Think OMO direction → reserves → Ms.” * **Explain others:** Short, specific rationale tied to misconception tags. --- ### That’s it. * Deploy → paste the iframe on Squarespace → you have a working MVP. * When ready: we’ll add diagnostics, spaced retrieval, and a dashboard.