Files
novi-lessons/Les11-AI-SDK/Les11-Docenttekst.md
2026-05-19 18:50:11 +02:00

815 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.