# Les 11 — Vercel AI SDK
## Docenttekst (Klas A — 3 uur, fysiek, demo-driven)

**Les:** 11 van 18
**Onderwerp:** Vercel AI SDK — AI features bouwen, gekoppeld aan eigen dataset
**Duur:** 180 minuten
**Format:** Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = thuis.
**Demo-app:** Polderfest 2027 — fictief muziekfestival, 500 records in Supabase.

---

## Hoe deze tekst werkt

Dit document is een **lopend script**. Je kunt 'm letterlijk volgen op je laptop terwijl je lesgeeft.

- `[SLIDE X]` — Klik naar slide X op de beamer
- `[SCHERM: slides | terminal | editor | browser | supabase]` — Welk scherm op de beamer
- **Vertel:** "..." — Letterlijk wat je zegt (mag in eigen woorden)
- `*[stage direction]*` — Korte instructie voor jezelf, niet uitspreken
- Code blocks = wat je typt
- 💬 = verwachte studentenvraag

---

## VÓÓR DE LES — Setup (60 min)

### 1. Tools open op je laptop
- VS Code / Cursor — leeg
- Terminal — open in `~/`
- Browser tabs:
  - https://supabase.com/dashboard (ingelogd)
  - https://platform.openai.com (key paraat)
  - `localhost:3000` tab (nog niets)
  - Dit docenttekst-bestand
  - De slides PDF / PPTX

### 2. Demo-repo `polderfest-demo` voorbereiden
```bash
cd ~
npx create-next-app@latest polderfest-demo \
  --typescript --tailwind --app --eslint --no-src-dir --turbopack
cd polderfest-demo
npm i @supabase/supabase-js ai @ai-sdk/openai zod dotenv
npm i tsx --save-dev
git init && git add . && git commit -m "init"
```

### 3. Nieuwe Supabase project
- Dashboard → **New Project** → naam `polderfest-demo`
- Wacht ~2 min op deploy
- Settings → API → kopieer URL + anon key + service role key
- `.env.local`:
  ```
  NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
  NEXT_PUBLIC_SUPABASE_ANON_KEY=...
  SUPABASE_SERVICE_ROLE_KEY=...
  OPENAI_API_KEY=sk-proj-...
  ```

### 4. Schema runnen
- Supabase → SQL Editor → plak `schema.sql` → Run
- Check Table Editor → `bands` tabel bestaat, leeg

### 5. Seed test (verwijder daarna)
- Plaats `seed-polderfest.ts` in `polderfest-demo/scripts/`
- `npx tsx scripts/seed-polderfest.ts` → 500 records erin → check Table Editor
- **Wis** alle records vóór de les: `delete from bands;` (zodat je live kunt seeden in demo 2)
- **Verwijder** `app/chat/page.tsx` + `app/api/chat/route.ts` (maken we live in demo 3)

### 6. Backup
- Zip van werkende eindstaat → op USB
- Check OpenAI usage dashboard — key werkt + credits aanwezig

---

# HET SCRIPT — Lees mee tijdens de les

## BLOK 1 — Welkom + Terugblik (10 min)

`[SLIDE 1]` `[SCHERM: slides]`

**Vertel:** "Welkom bij les 11. Vandaag de Vercel AI SDK — eerste keer dat we échte AI features gaan bouwen IN onze apps. Geen ChatGPT openen meer — AI in onze eigen code."

`[SLIDE 2 — Terugblik]`

**Vertel:** "Even kort terug: vorige lessen hebben we Supabase geïntegreerd. Tabellen en relaties opgezet. RLS-policies bekeken — wie mag wat lezen en schrijven.

Vandaag iets anders. We gaan **niet** voortbouwen op QuickPoll. We beginnen een nieuwe demo from scratch. Nieuwe Next.js app, nieuwe Supabase, en dan koppelen we daar de AI SDK aan."

`*[Wacht 2 sec, laat het landen]*`

`[SLIDE 3 — Planning]`

**Vertel:** "Dit is de planning. Drie uur. Eerst theorie — 30 minuten — wat is de AI SDK, welke modellen, welke functies. Daarna vier demo's. Eén: nieuwe app opzetten. Twee: 500 records in Supabase via een seed-script. Pauze. Drie: AI SDK installeren en chatten met de data. Vier: vragen stellen aan die data."

`*[Wijs naar de gele rij]*`

**Vertel:** "Belangrijk — dit is een **kijk-les**. Jullie typen vandaag niet mee. Pak je notitieboek of laptop voor aantekeningen. Thuis bouw je zelf een versie, met je eigen thema. Daar gaan de lesopdracht en huiswerk over."

💬 Verwachte vraag: *"Kunnen we niet meedoen?"*
Antwoord: "Liever niet — als jullie ook typen, gaat 't te langzaam en haakt iedereen op een ander moment af. Vanavond is voor zien-en-snappen. Thuis is doen."

---

## BLOK 2 — Theorie AI SDK (30 min)

`[SLIDE 4 — Wat is de AI SDK]` `[SCHERM: slides]`

**Vertel:** "Wat is de Vercel AI SDK? Een TypeScript-library die één unified API biedt voor alle AI-providers. OpenAI vandaag, Anthropic morgen, Google overmorgen, lokaal Ollama als je dat wil — je code blijft hetzelfde.

Open source. Gemaakt door Vercel — de makers van Next.js. Daarom: naadloze integratie met Server Components, Server Actions en streaming.

Wat zit er in:"

`*[Wijs naar de bullets]*`

**Vertel:** "Unified API. Streaming out-of-the-box — geen WebSocket-gedoe. React hooks zoals `useChat`. Tool Calling — komt volgende les. En type-safe gestructureerde output via Zod."

`*[Wijs naar het code-blok rechts]*`

**Vertel:** "Kijk hier. Dit is alle code die je nodig hebt voor één AI-call. Vier regels. En zie je `openai('gpt-4o-mini')`? Als ik dat morgen wil veranderen naar Anthropic — verander ik dat in `anthropic('claude-sonnet-4')`. Eén regel. Rest van mijn code blijft hetzelfde. Dat is de waarde."

`[SLIDE 5 — Modellen + kosten]`

**Vertel:** "Het modellen-landschap. Loop ik even langs:"

`*[Wijs per rij]*`

- "**gpt-4o-mini** — je default. Snel, goedkoop, $0.15 input / $0.60 output per miljoen tokens. Goed voor 80% van de use cases."
- "**gpt-4o** — multimodal, kan plaatjes lezen. 15× duurder dan mini. Pas pakken als nodig."
- "**gpt-4.1** — beste reasoning. Voor agents. Volgende lessen relevant."
- "**claude-sonnet-4** — Anthropic. Beter in coding, 200k context — dus lange documenten."
- "**gemini-2.5-flash** — Google. Ultra goedkoop, multimodal."
- "**llama-3.3-70b** op Groq — open-source model op snelste inference platform."

**Vertel:** "Vuistregel: start met gpt-4o-mini. Werkt 't niet goed genoeg? Probeer gpt-4o. Pas daarna iets exotisch. Premature optimization is een valkuil — het is letterlijk één regel veranderen om te wisselen, dus geen reden om voorbarig te kiezen."

`*[Wijs naar de blauwe callout]*`

**Vertel:** "Onze hele les vandaag, inclusief Polderfest met 500 bands en 10 vragen? Ongeveer 1 tot 2 cent. Echt. Schaalt prima."

`[SLIDE 6 — 4 kern-functies]`

**Vertel:** "De vier kern-functies van de SDK. Deze tabel is je cheat-sheet."

`*[Wijs per rij]*`

- "**generateText** — wachten tot AI klaar is, dan krijg je een string. Voor korte server-only calls."
- "**streamText** — streamt karakter voor karakter. Werkt met useChat. Dit gebruiken we vandaag."
- "**useChat** — React hook. Complete chat UI in 10 regels. Ook vandaag."
- "**generateObject** — type-safe data via Zod schema. Voor database-inserts of classificatie. Vandaag niet — komt later."

**Vertel:** "Onthoud: streamText en useChat — onze combo voor vandaag. generateObject zien jullie volgende lessen terug. Tool Calling — onderaan — dat is volgende les."

---

## BLOK 3 — Live Demo 1: Next.js + Supabase scaffold (20 min)

`[SLIDE 7 — Polderfest concept]` `[SCHERM: slides]`

**Vertel:** "Voor we gaan coderen — wat bouwen we eigenlijk?

We bouwen **Polderfest 2027**. Een fictief Nederlands muziekfestival. 500 verzonnen bands. Allemaal namen die niet bestaan. Geen Spotify, geen Pitchfork — pure fantasy."

`*[Wijs naar de gele 'Waarom een fictief festival' callout]*`

**Vertel:** "Waarom fictief? Omdat **geen enkele LLM** dit kan weten. Geen training data over Polderfest 2027 — bestaat niet. En dat is precies wat we willen demonstreren: AI alleen kan dit niet. AI mét onze data, wél."

`*[Wijs naar het schema-blok]*`

**Vertel:** "Onze tabel heeft deze velden: naam, genre, sub-genre, stage, dag, starttijd, stad, members, bio, tier, populariteit, ticket-impact. Genoeg variatie voor leuke vragen straks."

`*[Wijs naar voorbeeld-vragen]*`

**Vertel:** "Dingen die we straks aan onze AI gaan vragen: welke bands spelen vrijdagavond op de Main Stage? Vat de hip-hop scene samen. Welke acts komen uit Groningen? Allemaal vragen die ChatGPT niet kan beantwoorden — want hij weet niets van Polderfest. Maar onze chat straks wel."

`*[Pauze, ademen]*`

**Vertel:** "Goed — laten we 't gaan bouwen. Eerst Next.js en Supabase opzetten."

---

`[SLIDE 8 — LIVE DEMO 1]` `[SCHERM: slides]`

**Vertel:** "Dit zijn de 6 stappen die we nu gaan doorlopen. Ongeveer 20 minuten. Volg even mee — niet meetypen, kijk."

`[SCHERM: terminal]`

**Vertel:** "We beginnen in de terminal."

#### Stap 1 — Next.js scaffolden

```bash
cd ~
npx create-next-app@latest polderfest-demo \
  --typescript --tailwind --app --eslint --no-src-dir --turbopack
```

`*[Druk enter, wacht ~30 sec]*`

**Vertel terwijl het installeert:** "Standaard Next.js 15 met App Router, Tailwind, TypeScript. App Router omdat we Server Components willen. Tailwind voor styling. Niets bijzonders aan deze setup — dit kennen jullie al uit eerdere lessen."

`*[Wacht tot install klaar is]*`

```bash
cd polderfest-demo
code .
```

`[SCHERM: editor]`

**Vertel:** "Editor open. Niets in `app/page.tsx` — standaard Next.js welkomstpagina. Standaard `app/layout.tsx`. Tailwind config. Niks bijzonders."

#### Stap 2 — Supabase project aanmaken

`[SCHERM: browser → supabase.com/dashboard]`

**Vertel:** "Nu Supabase. Ik heb nog géén project voor deze demo — we maken er een nieuwe."

`*[Klik New Project]*`

**Vertel:** "Naam: `polderfest-demo`. Database password — kies wat, hoeft niet kopiëren. Region: West Europe. Submit."

`*[Wacht ~2 min — gebruik deze tijd voor de uitleg hieronder]*`

**Vertel:** "Terwijl het deployt: waarom een nieuw project? Omdat we van scratch beginnen. Geen vermenging met je QuickPoll-data van eerdere lessen. Clean slate. Voor je eindopdracht en huiswerk geldt: één app = één Supabase project."

#### Stap 3 — Schema runnen

`*[Supabase deploy is klaar]*`

`[SCHERM: supabase → SQL Editor]`

**Vertel:** "Schema-tijd. Open SQL Editor. New Query."

`*[Plak inhoud van schema.sql]*`

**Vertel:** "Dit is mijn schema voor de bands-tabel. Naam, genre, stage, dag, tijd, members, bio. Een paar indexen voor performance. RLS aan en een policy: bands zijn publiek leesbaar. Voor onze chat hebben we read-access nodig, geen schrijfrechten."

`*[Klik Run]*`

`[SCHERM: supabase → Table Editor]`

**Vertel:** "Check — tabel `bands` bestaat. Leeg. Klaar om gevuld te worden."

#### Stap 4 — Env vars

`[SCHERM: supabase → Settings → API]`

**Vertel:** "Drie dingen pak ik hier op: de Project URL, de anon public key, en de service_role secret key. Die laatste is belangrijk — straks bij het seed-script."

`*[Kopieer alle drie]*`

`[SCHERM: editor → .env.local]`

```
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
OPENAI_API_KEY=sk-proj-...
```

**Vertel:** "Belangrijke nuance — let goed op de namen:

- `NEXT_PUBLIC_SUPABASE_URL` en `_ANON_KEY` — die staan in client-bundle. Mag — anon key heeft alleen leesrechten via RLS.
- `SUPABASE_SERVICE_ROLE_KEY` — geen `NEXT_PUBLIC_` prefix. Server-only. Deze key bypasst RLS — daarmee kan alles. Lekken = ramp. Gebruiken we alleen lokaal voor het seed-script.
- `OPENAI_API_KEY` — geen `NEXT_PUBLIC_` prefix. Anders zit-ie in je client-bundle en kan iedereen 'm gebruiken op jouw kosten. Server-only altijd."

#### Stap 5 — Supabase client

`[SCHERM: editor]`

`*[Nieuwe file: lib/supabase.ts]*`

```typescript
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
```

**Vertel:** "Standaard Supabase client. Voor onze chat — dat is alles wat we nodig hebben. Niets bijzonders. Kennen jullie."

#### Stap 6 — Dev server check

`[SCHERM: terminal]`

```bash
npm run dev
```

`[SCHERM: browser → localhost:3000]`

**Vertel:** "Standaard Next.js welkomstpagina. Werkt. Supabase staat. Schema staat. Klaar voor data."

---

## BLOK 4 — Live Demo 2: Seed script — 500 records (20 min)

`[SLIDE 9 — LIVE DEMO 2]` `[SCHERM: slides]`

**Vertel:** "Demo 2. We gaan onze bands-tabel vullen. Niet handmatig — met een seed-script. 500 records in ~30 seconden."

`[SCHERM: editor]`

`*[Maak folder: scripts/. Plaats seed-polderfest.ts erin]*`

#### Stap 1 — Het seed-script bekijken

`[SCHERM: editor → seed-polderfest.ts]`

**Vertel:** "Dit is mijn seed-script. 200 regels. Laat me even door de structuur lopen — niet alle regels lezen, alleen de aanpak."

`*[Scroll naar top]*`

**Vertel:** "Bovenaan: Supabase client. Belangrijk — met de **service role key**. Niet anon. Want we gaan inserts doen. RLS blokkeert dat anders."

`*[Scroll naar de bouwstenen arrays]*`

**Vertel:** "Hier zijn mijn bouwstenen. Adjectives — 'Lost', 'Velvet', 'Iron', 'Neon'. Nouns — 'Tigers', 'Wolves', 'Mirrors'. Cities — Nederlandse steden. Genres — muziekgenres."

`*[Scroll naar generateBandName]*`

**Vertel:** "Hier de naam-generator. Vier patronen:
- 'Lost Tigers' — adjective + noun
- 'De Wolves' — Dutch prefix + noun
- 'Sanne Van Dijk' — solo artist
- 'Sanne & The Wolves' — solo + collectief

Met 30 adjectives × 30 nouns = al 900 unieke combinaties mogelijk. Genoeg voor 500 records."

`*[Scroll naar generateBio]*`

**Vertel:** "Bio's. Drie blokken — opening, middle, ending — gecombineerd. 'Begonnen in een garage in [stad]', '[band] experimenteert met analoge synths', 'Debuut-EP eind 2027'. Compositioneel. Geen handmatig getypte bio's — 500× zou krankzinnig zijn."

`*[Scroll naar bottom — async function seed]*`

**Vertel:** "De main functie. Genereert 500 bands, dedupe op naam, insert in batches van 100 — Supabase trekt 500 in één keer niet altijd. Done."

#### Stap 2 — Service role key uitleggen

**Vertel:** "Even pauze voor één belangrijk ding — de service role key. Die zit boven aan dit script. Drie regels die jullie moeten onthouden:

1. **Alleen lokaal gebruiken.** Niet in productie code. Niet in client. Alleen scripts.
2. **Nooit committen** naar git. Service role key in `.env.local`, en `.env.local` in `.gitignore`.
3. **Lekt-ie?** Direct draaien in Supabase dashboard → Settings → API → Reset service role key.

Vergelijk het met een root password. Behandel 'm zo."

#### Stap 3 — Dependencies (was al klaar — kort tonen)

`[SCHERM: terminal]`

```bash
# We hebben deze al uit setup, maar voor je eigen project:
npm i @supabase/supabase-js dotenv
npm i tsx --save-dev
```

#### Stap 4 — Run het seed-script

`[SCHERM: terminal]`

```bash
npx tsx scripts/seed-polderfest.ts
```

**Vertel terwijl het runt:** "Daar gaat 'ie. tsx is een TypeScript-runner die geen build-stap nodig heeft. dotenv leest de `.env.local` automatisch. 500 bands genereren, vijf batches van 100, klaar."

`*[Wacht ~10-30 sec, output verschijnt]*`

```
Genereren van 500 Polderfest bands...
Schrijven naar Supabase in batches van 100...
  ✓ 100/500
  ✓ 200/500
  ✓ 300/500
  ✓ 400/500
  ✓ 500/500
Klaar! 500 Polderfest bands staan in Supabase.
```

**Vertel:** "Done."

#### Stap 5 — Verificatie

`[SCHERM: supabase → Table Editor → bands]`

`*[Klik refresh — 500 records verschijnen]*`

**Vertel:** "500 bands. Allemaal verzonnen. Laten we er even één openklikken."

`*[Klik op willekeurige rij, toon bio]*`

**Vertel:** "Kijk — 'Begonnen in een garage in Groningen, De Tigers experimenteert met analoge synths en gefluisterde lyrics. Polderfest is hun grootste festival tot nu toe.' Compleet verzonnen. Geen Wikipedia, geen Spotify — pure fantasy. Maar overtuigend genoeg voor onze AI om mee te werken."

#### Stap 6 — Quick check met SQL

`[SCHERM: supabase → SQL Editor]`

```sql
select genre, count(*) from bands group by genre order by count desc;
```

`*[Run]*`

**Vertel:** "Genre-verdeling. ~30 per genre. Mooi gespreid. Klaar om mee te chatten."

---

## BLOK 5 — Pauze (15 min)

`[SLIDE 10 — Pauze]` `[SCHERM: slides]`

**Vertel:** "Pauze. 15 minuten. Tot zo."

`*[Coffee. Stretch. Check je OpenAI key nog even.]*`

---

## BLOK 6 — Live Demo 3: AI SDK + chat-route (30 min)

`[SLIDE 11 — LIVE DEMO 3]` `[SCHERM: slides]`

**Vertel:** "Welkom terug. Nu de echte AI-stap. We bouwen een chat-route in onze API en een chat-pagina in Next.js. Daarmee kunnen we vragen stellen aan onze Polderfest-data."

`[SCHERM: terminal]`

#### Stap 1 — Packages

```bash
npm i ai @ai-sdk/openai zod
```

**Vertel:** "Drie packages. `ai` is de SDK zelf. `@ai-sdk/openai` is de provider — we gebruiken OpenAI vandaag. `zod` is voor schema validatie. Vandaag gebruiken we 'm niet, maar volgende les wel."

#### Stap 2 — Chat API route

`[SCHERM: editor]`

`*[Maak file: app/api/chat/route.ts. Typ live, niet pasten — geeft studenten tijd om te volgen]*`

```typescript
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

export async function POST(req: Request) {
  const { messages } = await req.json();
```

**Vertel:** "API route. POST, want we ontvangen messages van de chat. We destructuren de messages-array."

```typescript
  // 1. Haal alle bands op uit Supabase
  const { data: bands, error } = await supabase.from("bands").select("*");
  if (error) throw error;
```

**Vertel:** "Stap 1 — ALLE bands ophalen uit Supabase. Voor 500 records werkt dit prima. Voor 50.000 niet — volgende les lossen we dat op met Tool Calling. Vandaag: simpele aanpak, alles meesturen."

```typescript
  // 2. Format bands als context-string
  const context = bands!
    .map((b) =>
      `- ${b.name} (${b.genre}, ${b.tier}, ${b.day} ${b.start_time} ` +
      `op ${b.stage}, uit ${b.origin_city})`
    )
    .join("\n");
```

**Vertel:** "Stap 2 — we maken één grote tekst-context. Per band één regel met de belangrijkste velden. AI kan namelijk geen SQL, maar wel tekst lezen."

```typescript
  // 3. System prompt met context
  const system = `Je bent een festival-assistent voor Polderfest 2027.
Hier zijn alle bands die op het festival spelen:

${context}

Beantwoord vragen van bezoekers over de line-up. Verzin niets — gebruik
alleen bovenstaande data. Antwoord in het Nederlands. Wees beknopt.`;
```

**Vertel:** "Stap 3 — de system prompt. Dit is de **rol** die AI krijgt. Drie belangrijke instructies:
1. 'Verzin niets' — voorkomt hallucinaties.
2. 'Gebruik alleen bovenstaande data' — niet uit training-kennis halen.
3. 'Antwoord in het Nederlands' — anders krijg je Engels.

Een goede system prompt is je hefboom. 50% van de kwaliteit komt hier."

```typescript
  // 4. Stream naar OpenAI
  const result = streamText({
    model: openai("gpt-4o-mini"),
    system,
    messages,
  });

  return result.toDataStreamResponse();
}
```

**Vertel:** "Stap 4 — de AI-call zelf. `streamText` — onze keuze van vandaag. Model gpt-4o-mini. System message, plus de berichten van de user. En `result.toDataStreamResponse()` zet 't om naar het juiste streaming-format voor useChat aan de client-kant."

**Vertel:** "API route klaar."

#### Stap 3 — Chat pagina

`*[Nieuwe file: app/chat/page.tsx]*`

```tsx
"use client";

import { useChat } from "ai/react";

export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, status } =
    useChat();

  return (
    <main className="max-w-2xl mx-auto p-6 flex flex-col h-screen">
      <h1 className="text-2xl font-bold mb-4">Polderfest 2027 — vraag de AI</h1>

      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((m) => (
          <div
            key={m.id}
            className={
              m.role === "user"
                ? "bg-blue-50 p-3 rounded-lg ml-12"
                : "bg-gray-50 p-3 rounded-lg mr-12"
            }
          >
            <div className="font-medium text-sm text-gray-500 mb-1">
              {m.role === "user" ? "Jij" : "Festival AI"}
            </div>
            <div className="whitespace-pre-wrap">{m.content}</div>
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Stel een vraag over de line-up..."
          className="flex-1 p-3 border rounded-lg"
          disabled={status !== "ready"}
        />
        <button
          type="submit"
          disabled={status !== "ready"}
          className="px-6 py-3 bg-blue-600 text-white rounded-lg disabled:opacity-50"
        >
          Stuur
        </button>
      </form>
    </main>
  );
}
```

**Vertel:** "Client component — `'use client'` bovenaan. `useChat` hook regelt alles: messages-state, input-state, submit-handler, streaming. Vijf properties, geen extra useState nodig.

UI is bewust simpel. Tailwind classes. Berichten van user blauw rechts, AI grijs links. Input + verzenden onderaan. Disabled tijdens streaming via `status !== 'ready'`."

#### Stap 4 — Testen

`[SCHERM: browser → localhost:3000/chat]`

**Vertel:** "Naar `/chat`. Eerste vraag."

`*[Typ in chat-input]*`

```
Hallo, wie ben jij?
```

`*[Druk Enter. AI antwoordt streamend.]*`

**Vertel terwijl AI antwoordt:** "Daar gaat 'ie. Karakter voor karakter. Streamt. Veel sneller voelend dan wachten op heel antwoord. UseChat regelt het, je hoeft niks zelf te doen voor streaming."

**Vertel:** "Klaar. Werkt. Nu de leukste vraag: vragen aan onze data."

---

## BLOK 7 — Live Demo 4: Vragen aan onze data (15 min)

`[SLIDE 12 — LIVE DEMO 4]` `[SCHERM: slides]`

**Vertel:** "We gaan vijf vragen stellen. Eén voor één. Bij elke vraag — let op wat de AI doet, en hoe dat anders is dan een SQL query of een gewone chatbot."

`[SCHERM: browser → /chat]`

#### Vraag 1 — Filter

`*[Type in chat]*`

```
Welke bands spelen zaterdag op de Beach Stage?
```

`*[AI antwoordt — geeft een lijst]*`

**Vertel:** "Filter. AI heeft door de tekst-context gefilterd. Even bevestigen met SQL —"

`[SCHERM: supabase → SQL Editor]`

```sql
select name, start_time from bands
where day = 'Zaterdag' and stage = 'Beach Stage';
```

`*[Run]*`

**Vertel:** "Zelfde resultaat. Maar het verschil — onze chat geeft natuurlijke taal, kan vervolgvragen aan, kan samenvatten. SQL doet alleen filter + select."

#### Vraag 2 — Sort + Reasoning

`[SCHERM: browser → /chat]`

```
Geef me 3 headliners met de meeste popularity, en hun bio's
```

`*[AI antwoordt]*`

**Vertel:** "Sort. AI heeft op `popularity` gesorteerd en de top 3 gepakt. Plus de bio's erbij."

`*[Vraag 2b in zelfde chat:]*`

```
En welke daarvan zou je aanraden voor iemand die houdt van techno?
```

`*[AI antwoordt — beargumenteerd]*`

**Vertel:** "Dit is **reasoning**. AI redeneert over genre + sub-genre + bio-tekst om een aanbeveling te doen. Een SQL query kan dit niet. Dit is waar AI waarde toevoegt bovenop pure data."

#### Vraag 3 — Aggregate

```
Hoeveel jazz fusion acts spelen er totaal op Polderfest?
```

`*[AI antwoordt met een getal]*`

**Vertel:** "Aggregate. AI heeft geteld. Even SQL-bevestigen —"

`[SCHERM: supabase → SQL Editor]`

```sql
select count(*) from bands where genre = 'Jazz Fusion';
```

**Vertel:** "Klopt. Goed."

#### Vraag 4 — Samenvatting (waar AI uitblinkt)

`[SCHERM: browser → /chat]`

```
Vat de electronic-scene op Polderfest samen in 3 zinnen
```

`*[AI antwoordt met een echte samenvatting]*`

**Vertel:** "Hier is geen SQL voor. Dit is **samenvatten**. AI heeft alle electronic acts gelezen, gewogen, en in 3 zinnen samengevat. Dit is de unieke kracht van AI bovenop data."

#### Vraag 5 — De mist in (bewust)

```
Wie was de hoofdact van Polderfest 2025?
```

`*[AI antwoordt eerlijk dat hij dit niet weet]*`

**Vertel:** "Goed. Geen verzinsels. AI zegt: 'mijn data is alleen 2027, dat zit er niet in'. Dat is de kracht van onze system prompt. Zonder die prompt zou-ie waarschijnlijk wat hallucineren."

`*[Pauze, kijk de klas in]*`

**Vertel:** "Zien jullie wat hier gebeurt? Geen LLM ter wereld kent Polderfest 2027. Geen Wikipedia, geen training-data. Maar onze chat beantwoordt alles — omdat **wij** de data leveren. AI + data = product."

---

## BLOK 8 — Data + AI = kracht (5 min)

`[SLIDE 13 — Data + AI = kracht]` `[SCHERM: slides]`

**Vertel:** "Reflectie-moment. Drie scenario's:"

`*[Wijs naar de grijze box]*`

**Vertel:** "**Data alleen.** Wat heb je? SQL queries. Filter, sort, select. Geen taal, geen interpretatie. Gebruiker moet zelf SQL kunnen."

`*[Wijs naar de roze box]*`

**Vertel:** "**AI alleen.** ChatGPT zonder context. Generieke kennis uit training. Hallucineert. Geen privé data, geen live data."

`*[Wijs naar de blauwe box]*`

**Vertel:** "**Data + AI.** Wat we vandaag bouwden. Filter via reasoning. Antwoorden in natuurlijke taal. Samenvattingen, vergelijkingen, aanbevelingen. Schaalbaar — voeg data toe en je hebt nieuwe antwoorden mogelijk."

`*[Pauze]*`

**Vertel:** "Quote om mee weg te lopen:"

`*[Wijs naar de gele callout]*`

**Vertel:** "*Een LLM zonder jouw data is een gewone chatbot. Een LLM mét jouw data is een product.* Onthoud dit. Dit is de fundering van alle volgende lessen."

---

## BLOK 9 — Lesopdracht + Huiswerk uitleg (20 min)

`[SLIDE 14 — Lesopdracht]` `[SCHERM: slides]`

**Vertel:** "Lesopdracht. Voor thuis — niet vanavond per se, maar vóór volgende les. Je bouwt **een eigen versie** van wat we vandaag deden. Met **je eigen thema**."

`*[Loop checklist langs op slide]*`

**Vertel:** "Eisen op een rij. Bedenk een eigen thema — moet fictief zijn. Nieuw Next.js project, nieuw Supabase. Eigen seed-script. Minstens 100 records. Chat-route en chat-pagina werkend. Drie vragen stellen die alleen werken dankzij jouw data."

`*[Wijs naar pink callout]*`

**Vertel:** "Inspiratie: fictief restaurant-aggregator in een verzonnen stad. Scriptie-archief van NOVI met 1000 nep-titels. Museumcollectie met verzonnen kunstenaars. D&D NPCs. Cryptid-sightings in Nederland.

Belangrijk: **moet fictief zijn**. Als je echte restaurants in Amsterdam pakt, weet ChatGPT die al — dan zie je niet wat we vandaag demonstreerden. Het hele punt is: data die geen LLM kent."

💬 Verwachte vraag: *"Mag ik echt elk thema?"*
Antwoord: "Ja, zolang het fictief is en minstens 100 records heeft. Twijfel? Stuur 'm op Brightspace, dan kijk ik even mee."

`[SLIDE 15 — Huiswerk]`

**Vertel:** "Het huiswerk bouwt voort op de lesopdracht. Drie onderdelen — alle drie verplicht."

`*[Loop A, B, C langs op slide]*`

**Vertel:** "**Onderdeel A.** Pas het Polderfest seed-script aan voor jouw thema. Het script staat klaar als bijlage. Open 't, lees 't, en pas 't aan. AI mag je helpen — letterlijk: open OpenCode, plak m'n script erin, vraag 'pas dit aan voor [mijn thema]'. Klaar in een paar minuten. Daarna jij review. Minstens 200 records.

**Onderdeel B.** Voeg minstens 1 extra veld toe aan je schema. Iets dat een **nieuwe vraag** mogelijk maakt. Niet zomaar een extra string-kolom. Concreet voorbeeld: een museumcollectie met `acquisition_story` veld — dan kun je vragen 'welke kunstwerken zijn op een veiling gekocht?'. Update seed-script, re-seed, test in chat.

**Onderdeel C.** Schrijf een `AI-CHAT.md` in je repo-root. Drie secties:
- Mijn thema — wat is het, waarom kan een gewone LLM dit niet?
- 3 leuke vragen die werken
- 1 vraag waar AI moeite mee had + hoe je je prompt aanpaste."

`*[Wijs naar gele callout]*`

**Vertel:** "Bonus, geen verplichting: deploy op Vercel, loading skeleton, vergelijk gpt-4o-mini en gpt-4o."

💬 Verwachte vraag: *"Hoe lang gaat dit duren?"*
Antwoord: "Lesopdracht ~2,5 uur. Huiswerk ~1,5 tot 2 uur. Samen een lange middag. Loop je vast — Brightspace of plan een korte 1-op-1 met me."

---

## BLOK 10 — Vragen + Afsluiting (15 min)

`[SLIDE 16 — Volgende les: Tool Calling]` `[SCHERM: slides]`

**Vertel:** "Eén ding voor we afronden — wat komt hierna."

`*[Wijs naar pink callout: Het schaal-probleem]*`

**Vertel:** "Wat we vandaag deden: ALLE 500 bands sturen we mee als context bij elke vraag. Dat is ~30.000 tokens per call. Werkt prima voor 500. Werkt **niet** voor 5.000 records. En al helemaal niet voor 50.000."

`*[Wijs naar blauwe callout: De oplossing]*`

**Vertel:** "Volgende les — **Tool Calling**. In plaats van alle data meesturen, geef je AI **functies** die hij zelf kan aanroepen. Hij hoort vraag 'welke bands op vrijdag?' en besluit: ik roep `searchBands({ day: 'Vrijdag' })` aan. Krijgt 60 bands terug. Antwoordt. Schaalbaar tot duizenden records."

**Vertel:** "Daarna in deze leerlijn: Agents met maxSteps. RAG met embeddings — semantic search op heel grote datasets. Testing, deployment, performance. En de laatste twee lessen: eindopdracht-werkdagen en je pitch."

`[SLIDE 17 — Afsluiting]`

**Vertel:** "Vragen?"

`*[Open de vloer. Verwachte vragen + antwoorden:]*`

💬 *"Wat als ik geen schoolkey heb?"*
→ "Eigen OpenAI account — $5 starter credit zit erin gratis. Of Groq — gratis tier. Of Anthropic — $5 gratis credits."

💬 *"Hoe weet ik welk model het beste is?"*
→ "Start met gpt-4o-mini. Upgrade alleen als het écht niet werkt. Premature optimization is een valkuil."

💬 *"Kan dit lokaal zonder OpenAI?"*
→ "Ja, via Ollama. Niet vereist voor deze les. Komt eventueel in latere lessen."

💬 *"Moet ik de Polderfest demo zelf ook namaken?"*
→ "Nee. Wat we vandaag deden is jullie laten zien. Voor jezelf bouwen = eigen thema, in lesopdracht en huiswerk."

💬 *"Hoe duur is dit nou echt?"*
→ "Onze hele les vandaag met 500 bands en 10 vragen — onder de 2 cent. Met gpt-4o (de grote) zou hetzelfde ~30 cent zijn. Met mini blijft het peanuts."

`*[Sluit af]*`

**Vertel:** "Zorg dat je vóór volgende les minstens je seed-script werkend hebt voor jouw thema. Dan kunnen we volgende les meteen Tool Calling toepassen. Tot dan!"

---

## Backup-onderwerpen (als tijd over is)

1. **Andere provider tonen** — Open `route.ts`, vervang `openai("gpt-4o-mini")` door `anthropic("claude-sonnet-4")`. Werkt direct. Eén regel.
2. **System prompt fine-tuning** — Verzwak de prompt ("Help bij vragen"). Vraag iets. Verzin de fout. Versterk weer ("Verzin niets, gebruik alleen onze data"). Toon verschil.
3. **Token-kosten dashboard** — Open platform.openai.com/usage. Toon je verbruik van vandaag — letterlijk een paar cent.
4. **Privacy / data retention** — Wat gaat naar OpenAI? Zero-data-retention via EU-endpoints. Belangrijk voor productie.
5. **Hallucinatie-test** — Probeer met zwakke prompt of de AI iets verzint over Polderfest 2025. Toon dat sterkere prompt dit fixt.
