815 lines
30 KiB
Markdown
815 lines
30 KiB
Markdown
# 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.
|