# Les 11 — Vercel AI SDK
## Lesstof

**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Onderwerp:** Vercel AI SDK — AI features bouwen, gekoppeld aan eigen dataset
**Demo:** Polderfest 2027 — fictief festival, 500 records

---

## Inhoudsopgave

1. [Wat is de Vercel AI SDK?](#1-wat-is-de-vercel-ai-sdk)
2. [Het modellen-landschap](#2-het-modellen-landschap)
3. [De vier kern-functies](#3-de-vier-kern-functies)
4. [Project setup van A tot Z](#4-project-setup-van-a-tot-z)
5. [Seed script: dummy data in Supabase](#5-seed-script-dummy-data-in-supabase)
6. [Chat-route + chat-UI implementeren](#6-chat-route--chat-ui-implementeren)
7. [Waarom data + AI samen krachtig zijn](#7-waarom-data--ai-samen-krachtig-zijn)
8. [Best practices & valkuilen](#8-best-practices--valkuilen)
9. [Wat komt hierna? Tool Calling teaser](#9-wat-komt-hierna)
10. [Bronnen](#10-bronnen)

---

## 1. Wat is de Vercel AI SDK?

De Vercel AI SDK is een open-source TypeScript library waarmee je AI-features in je webapplicatie bouwt. Gemaakt door Vercel — de makers van Next.js — en daarom perfect geïntegreerd met React, Server Components en Server Actions.

### Wat krijg je?

- **Unified API** — Zelfde code voor elk model (40+ providers)
- **Streaming out-of-the-box** — Geen WebSocket-setup nodig
- **React hooks** — `useChat`, `useCompletion` voor instant chat UI
- **Tool Calling** — AI kan functies aanroepen die jij definieert (volgende les)
- **Structured output** — Type-safe data via `generateObject` + Zod
- **Multi-step agents** — Via `maxSteps`

### Eerste indruk

```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const { text } = await generateText({
  model: openai("gpt-4o-mini"),
  prompt: "Vat de Polderfest 2027 line-up samen",
});
```

Wil je naar Anthropic? Eén regel veranderen:

```typescript
import { anthropic } from "@ai-sdk/anthropic";

const { text } = await generateText({
  model: anthropic("claude-sonnet-4"),
  prompt: "Vat de Polderfest 2027 line-up samen",
});
```

Dat is de waarde.

---

## 2. Het modellen-landschap

| Provider | Model | Sterke punten | Prijs (in/out per 1M tokens) |
|----------|-------|---------------|------------------------------|
| OpenAI | `gpt-4o-mini` | Snel, goedkoop, default | $0.15 / $0.60 |
| OpenAI | `gpt-4o` | Multimodal (vision), krachtig | $2.50 / $10 |
| OpenAI | `gpt-4.1` | Reasoning, voor agents | $2 / $8 |
| Anthropic | `claude-sonnet-4` | Coding, lange context (200k) | $3 / $15 |
| Google | `gemini-2.5-flash` | Multimodal, ultra goedkoop | $0.075 / $0.30 |
| Groq | `llama-3.3-70b` | Ultra-fast inference | $0.59 / $0.79 |

**Vuistregel:** start met `gpt-4o-mini`. Werkt het niet goed? Upgrade naar `gpt-4o`. Dan pas exotisch.

**Wat kost onze Polderfest demo?**
- Context = 500 bands → ~30.000 tokens per chat-request
- 1 vraag = ~$0.005 (een halve cent)
- 50 vragen = $0.25 (kwart euro)

Met gpt-4o (15× duurder) zou dezelfde demo ~$4 kosten. Daarom: start mini.

---

## 3. De vier kern-functies

### `generateText` — Antwoord ophalen
Wacht tot het AI-antwoord compleet is, returnt dan een string.
```typescript
const { text } = await generateText({
  model: openai("gpt-4o-mini"),
  prompt: "Geef 3 koffie-poll opties",
});
```
**Wanneer:** Korte antwoorden, server-only, niet-interactief.

### `streamText` — Streaming antwoord
Streamt karakter voor karakter. Goed voor chat UI.
```typescript
const result = streamText({
  model: openai("gpt-4o-mini"),
  messages,
});
return result.toDataStreamResponse();
```
**Wanneer:** Chat UI, lange antwoorden, "ChatGPT-gevoel". Vandaag gebruiken we dit.

### `useChat` — React hook
Aan client-kant: complete chat UI in 10 regels.
```tsx
"use client";
import { useChat } from "ai/react";

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();
  return (
    <form onSubmit={handleSubmit}>
      {messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
      <input value={input} onChange={handleInputChange} />
    </form>
  );
}
```
**Belangrijk:** werkt alleen met `streamText` als API endpoint. Vandaag gebruiken we dit ook.

### `generateObject` — Gestructureerde data
In plaats van een string krijg je type-safe data terug — gevalideerd met Zod.
```typescript
const { object } = await generateObject({
  model: openai("gpt-4o-mini"),
  schema: z.object({
    question: z.string(),
    options: z.array(z.string()).length(4),
  }),
  prompt: "Maak een poll over koffie",
});
```
**Wanneer:** Database inserts, formulieren vullen, classificatie. **Niet** vandaag — komt terug in latere lessen.

---

## 4. Project setup van A tot Z

Dit is wat Tim in de les live deed. Voor je eigen project (lesopdracht/huiswerk): zelfde stappen.

### Stap 1 — Next.js scaffolden

```bash
npx create-next-app@latest mijn-thema \
  --typescript --tailwind --app --eslint --no-src-dir --turbopack
cd mijn-thema
```

### Stap 2 — Supabase project aanmaken

- Ga naar https://supabase.com/dashboard
- **New Project** → kies naam
- Wacht ~2 min op deploy
- Settings → API → kopieer:
  - Project URL
  - anon public key
  - service_role secret key

### Stap 3 — Schema definiëren

Pas `schema.sql` aan voor jouw thema. Bv:
```sql
create table items (
  id          bigserial primary key,
  name        text not null,
  category    text,
  rating      int,
  description text,
  created_at  timestamp default now()
);
```

Run in Supabase → SQL Editor.

### Stap 4 — Env variables

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

Belangrijke regels:
- `NEXT_PUBLIC_*` = client-leesbaar
- `SUPABASE_SERVICE_ROLE_KEY` = server-only, voor seed script (geen `NEXT_PUBLIC_`)
- `OPENAI_API_KEY` = server-only (geen `NEXT_PUBLIC_`)

### Stap 5 — Packages installeren

```bash
npm install @supabase/supabase-js ai @ai-sdk/openai zod
npm install --save-dev tsx dotenv
```

### Stap 6 — Supabase client

`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!,
);
```

---

## 5. Seed script: dummy data in Supabase

Een seed-script is een TypeScript bestand dat **éénmalig** je tabel vult met dummy data. Geen handmatige inserts — procedureel gegenereerd.

### Het Polderfest script (voorbeeld)

Het volledige `seed-polderfest.ts` zit als bijlage bij deze les. Kernidee:

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

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
);

const adjectives = ["Lost", "Velvet", "Iron", "Neon", "Silent", ...];
const nouns = ["Tigers", "Wolves", "Mirrors", "Clouds", ...];
const genres = ["Indie Rock", "Electronic", ...];

function generateBand(i: number) {
  return {
    name: `${pick(adjectives)} ${pick(nouns)}`,
    genre: pick(genres),
    // ...
  };
}

async function seed() {
  const bands = [];
  for (let i = 0; i < 500; i++) bands.push(generateBand(i));

  for (let i = 0; i < bands.length; i += 100) {
    await supabase.from("bands").insert(bands.slice(i, i + 100));
  }
}

seed();
```

### Runnen

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

### Waarom procedureel?

- **Met 500 hard-coded records** = 500 regels handmatige data → mind-numbing
- **Met combinaties** van 30 adjectives × 30 nouns = 900 unieke namen mogelijk
- **Met seed-random** = reproduceerbaar (zelfde data bij re-run)

### Voor jouw eigen thema

Open `seed-polderfest.ts`, kopieer de structuur, en vervang de **bouwstenen**:
- Domein-specifieke arrays (in plaats van bands: restaurants, scripties, kunstwerken…)
- Domein-specifieke velden
- Domein-specifieke bio/beschrijving-fragmenten

**Pro tip:** vraag een AI om dit te doen! "Pas het Polderfest seed-script aan voor [thema]." OpenCode of Cursor doet dit in 30 seconden.

---

## 6. Chat-route + chat-UI implementeren

Dit is wat **alleen nieuw** is — Next.js + Supabase kennen jullie al.

### De chat-route

`app/api/chat/route.ts`:

```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();

  // 1. Haal data op
  const { data: bands } = await supabase.from("bands").select("*");

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

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

${context}

Beantwoord vragen op basis van bovenstaande data. Verzin niets.
Antwoord in het Nederlands.`;

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

  return result.toDataStreamResponse();
}
```

### De chat-pagina

`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"
        />
        <button
          type="submit"
          className="px-6 py-3 bg-blue-600 text-white rounded-lg"
        >
          Stuur
        </button>
      </form>
    </main>
  );
}
```

### Voorbeeld-vragen die we live stelden

| Vraag | Wat AI doet |
|-------|-------------|
| "Welke bands spelen zaterdag op de Beach Stage?" | Filtert door context |
| "3 headliners met meeste populariteit" | Sorteert + select top |
| "Hoeveel jazz fusion acts totaal?" | Telt |
| "Vat de electronic-scene samen" | **Samenvatting** — alleen AI kan dit |
| "Wie was hoofdact van Polderfest 2025?" | Eerlijk: "weet ik niet" — perfect |

---

## 7. Waarom data + AI samen krachtig zijn

### Data alleen
- SQL queries: filter, sort, select
- Geen interpretatie, geen taal, geen samenvatting
- Gebruiker moet zelf SQL kunnen

### AI alleen
- Kennis is generiek (training data)
- Verzint vaak (hallucinatie)
- Geen toegang tot live data of private data

### Data + AI
- AI filtert via reasoning op tekst-context
- Antwoorden in natuurlijke taal
- Samenvattingen en interpretatie
- Domein-kennis = jouw data, AI redeneert erover

> "Een LLM zonder jouw data is een gewone chatbot.
> Een LLM mét jouw data is een product."

---

## 8. Best practices & valkuilen

### Doen

- **Begin met `gpt-4o-mini`** — upgrade pas als nodig
- **System prompt is essentieel** — "gebruik alleen onze data, verzin niets"
- **Stream alles** — `streamText` voelt 5× sneller dan `generateText`
- **AI-calls altijd server-side** — keys blijven veilig
- **Loading state** — AI duurt 1-5 sec, zonder feedback voelt het stuk
- **Foutafhandeling** — `try/catch` rond elke AI-call

### Niet doen

- **Geen `NEXT_PUBLIC_OPENAI_API_KEY`** — wordt zichtbaar in client
- **Niet de output blind vertrouwen** — AI hallucineert
- **Niet alle data altijd meesturen** — werkt voor 500 records, niet voor 50.000 (volgende les)
- **Niet `gpt-4o` als default** — 15× duurder dan mini, vaak onnodig

### Veelvoorkomende fouten

| Fout | Oorzaak | Oplossing |
|------|---------|-----------|
| `OPENAI_API_KEY is not defined` | `.env.local` niet geladen | Dev server herstarten |
| `Cannot find module 'ai'` | npm install vergeten | `npm i ai @ai-sdk/openai` |
| Seed: `permission denied` | Anon key i.p.v. service role | Gebruik `SUPABASE_SERVICE_ROLE_KEY` |
| AI antwoordt in Engels | Niet expliciet om NL gevraagd | System prompt aanpassen |
| AI verzint feiten | System prompt te zwak | Voeg toe: "verzin niets, gebruik alleen onze data" |
| Chat laadt niet | `useChat` zonder `streamText` API | Endpoint moet `result.toDataStreamResponse()` returnen |

---

## 9. Wat komt hierna?

### Het schaalprobleem

Vandaag sturen we **alle 500 bands** mee als context bij elke request. Dat is ~30k tokens. Werkt prima voor 500. Werkt **niet** voor:
- 5.000 records → te duur, te traag
- 50.000 records → past niet in context window
- Real-time data → context wordt steeds opnieuw gebouwd

### Volgende les: Tool Calling (Les 12)

In plaats van **alle data te sturen**, geef je de AI **tools** (functies). De AI besluit zelf welke te gebruiken:

```typescript
const { text } = await generateText({
  model: openai("gpt-4o-mini"),
  messages,
  tools: {
    searchBands: tool({
      description: "Zoek bands op dag, stage, of genre",
      parameters: z.object({
        day: z.string().optional(),
        stage: z.string().optional(),
        genre: z.string().optional(),
      }),
      execute: async ({ day, stage, genre }) => {
        // Supabase query
        const { data } = await supabase.from("bands").select("*")
          .eq("day", day || undefined)
          .eq("stage", stage || undefined);
        return data;
      },
    }),
  },
  maxSteps: 5,
});
```

Workflow:
1. User: "Welke bands op vrijdag?"
2. AI: "Ik roep `searchBands({ day: 'Vrijdag' })` aan"
3. Supabase: 60 bands terug
4. AI: "Op vrijdag spelen 60 bands. De headliners zijn..."

Schaalbaar. Slim. Multi-step (combineer meerdere tools).

### Daarna in deze leerlijn

- **Les 13:** Agents + `maxSteps` (autonome multi-step taken)
- **Les 14:** RAG + embeddings (semantic search op heel grote datasets)
- **Les 15-16:** Testing + Deployment + Performance
- **Les 17-18:** Eindopdracht-werkdagen + Pitch

---

## 10. Bronnen

### Vercel AI SDK
- Hoofdpagina: https://ai-sdk.dev/docs/introduction
- Voorbeelden: https://ai-sdk.dev/examples
- `streamText`: https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text
- `useChat`: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat
- Tool Calling (volgende les): https://ai-sdk.dev/docs/foundations/tools

### Supabase
- JS Client: https://supabase.com/docs/reference/javascript
- Row Level Security: https://supabase.com/docs/guides/auth/row-level-security
- Server-side usage: https://supabase.com/docs/guides/auth/server-side

### Inspiratie
- v0.dev — Generative UI in actie
- chat.vercel.ai — Officiële demo van AI SDK
- Vercel templates met AI: https://vercel.com/templates?type=ai

### Tokens & kosten
- OpenAI pricing: https://openai.com/api/pricing
- Tokenizer: https://platform.openai.com/tokenizer
- Usage dashboard: https://platform.openai.com/usage
