add les 12

This commit is contained in:
2026-05-21 08:52:47 +02:00
parent 634789e615
commit eb1ba2e28d
42 changed files with 11012 additions and 8 deletions

BIN
Les11-AI-SDK/Les11-Slides.key Executable file

Binary file not shown.

68
Les11-AI-SDK/page.tsx Normal file
View File

@@ -0,0 +1,68 @@
/**
* Polderfest 2027 — chat pagina
* --------------------------------------------------
* Les 11 — useChat hook + Tailwind chat UI.
* Plaats dit bestand op: app/chat/page.tsx
*
* Werking:
* - useChat() regelt messages, input, submit-handler, streaming
* - Praat met /api/chat (de route.ts)
* - Disabled tijdens streaming
*
* Vereist:
* - app/api/chat/route.ts (zie route.ts)
* - npm i ai
* - Tailwind aanwezig in project (standaard in create-next-app)
*/
"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>
);
}

Submodule Les11-AI-SDK/polderfest-demo added at ac06b4d59e

61
Les11-AI-SDK/route.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* Polderfest 2027 — chat API route
* --------------------------------------------------
* Les 11 — Vercel AI SDK + Supabase context.
* Plaats dit bestand op: app/api/chat/route.ts
*
* Werking:
* 1. Haal alle bands op uit Supabase
* 2. Formatteer als tekst-context
* 3. Stuur naar OpenAI via streamText + system prompt
* 4. Return een stream voor useChat
*
* Vereist:
* - NEXT_PUBLIC_SUPABASE_URL en NEXT_PUBLIC_SUPABASE_ANON_KEY in .env.local
* - OPENAI_API_KEY in .env.local
* - npm i ai @ai-sdk/openai @supabase/supabase-js
*/
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 alle bands op uit Supabase
const { data: bands, error } = await supabase.from("bands").select("*");
if (error) throw error;
// 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");
// 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.`;
// 4. Stream naar OpenAI
const result = streamText({
model: openai("gpt-4o-mini"),
system,
messages,
});
return result.toDataStreamResponse();
}

View File

@@ -15,13 +15,27 @@
*/
import { createClient } from "@supabase/supabase-js";
import "dotenv/config";
import dotenv from "dotenv";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { persistSession: false } }
);
// Laad .env.local (i.p.v. default .env)
dotenv.config({ path: ".env.local" });
const SUPABASE_URL =
process.env.NEXT_PUBLIC_SUPABASE_URL ?? process.env.SUPABASE_URL;
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
console.error(
"Ontbrekende env vars. Check .env.local:\n" +
" NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co\n" +
" SUPABASE_SERVICE_ROLE_KEY=<service role key>"
);
process.exit(1);
}
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: { persistSession: false },
});
// ────────────────────────────────────────────────────────────
// Deterministische random (zodat seed reproduceerbaar is)
@@ -189,7 +203,7 @@ function generateBio(name: string): string {
// ────────────────────────────────────────────────────────────
// Hoofdfunctie
// ────────────────────────────────────────────────────────────
async function seed() {
async function runSeed() {
console.log("Genereren van 500 Polderfest bands...");
// Wipe bestaande data (optioneel)
@@ -252,7 +266,7 @@ async function seed() {
console.log("Klaar! 500 Polderfest bands staan in Supabase.");
}
seed().catch((e) => {
runSeed().catch((e) => {
console.error(e);
process.exit(1);
});