add les 12
This commit is contained in:
97
Les12-Tool-Calling/polderfest-demo/app/api/chat/route.ts
Normal file
97
Les12-Tool-Calling/polderfest-demo/app/api/chat/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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 { convertToModelMessages, streamText, type UIMessage } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey =
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY ??
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl) {
|
||||
throw new Error("Missing env: NEXT_PUBLIC_SUPABASE_URL");
|
||||
}
|
||||
if (!supabaseKey) {
|
||||
throw new Error(
|
||||
"Missing env: SUPABASE_SERVICE_ROLE_KEY (preferred) or NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
|
||||
// 1. Haal alle bands op uit Supabase
|
||||
const { data: bands, error } = await supabase.from("bands").select("*");
|
||||
if (error) {
|
||||
return Response.json(
|
||||
{ error: "Supabase query failed", details: error.message },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
// 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}), populariteit: ${b.popularity}`,
|
||||
)
|
||||
.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. Geef puur antwoord op de gestelde vraag en voeg geen extra informatie toe.
|
||||
|
||||
Zodra iemand iets vraagt over het schema. Geef dan altijd een volledig schema terug met band, stage en start en eind tijd. Sorteer op start tijd.
|
||||
|
||||
Wanneer een band speelt op bijvoorbeeld vrijdagavond, geef hem dan niet weer als iemand vraagt om de bands van zaterdag.
|
||||
|
||||
Regels:
|
||||
- Zaterdag start om 16:00
|
||||
`;
|
||||
|
||||
// 4. Stream naar OpenAI
|
||||
const result = streamText({
|
||||
model: openai('gpt-5.2'),
|
||||
system,
|
||||
messages: await convertToModelMessages(messages),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse({
|
||||
onError(error: unknown) {
|
||||
if (error == null) return "unknown error";
|
||||
if (typeof error === "string") return error;
|
||||
if (error instanceof Error) return error.message;
|
||||
return JSON.stringify(error);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
return Response.json({ error: "Chat route failed", details: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
BIN
Les12-Tool-Calling/polderfest-demo/app/favicon.ico
Normal file
BIN
Les12-Tool-Calling/polderfest-demo/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
Les12-Tool-Calling/polderfest-demo/app/globals.css
Normal file
26
Les12-Tool-Calling/polderfest-demo/app/globals.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
33
Les12-Tool-Calling/polderfest-demo/app/layout.tsx
Normal file
33
Les12-Tool-Calling/polderfest-demo/app/layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
89
Les12-Tool-Calling/polderfest-demo/app/page.tsx
Normal file
89
Les12-Tool-Calling/polderfest-demo/app/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 { useState } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
|
||||
export default function ChatPage() {
|
||||
const { messages, sendMessage, status } = useChat();
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
return (
|
||||
<main className="max-w-2xl mx-auto p-6 flex flex-col h-screen">
|
||||
<div className="flex items-center justify-between gap-3 mb-4">
|
||||
<h1 className="text-2xl font-bold">Polderfest 2027 — vraag de AI</h1>
|
||||
<a
|
||||
href="/supabase"
|
||||
className="text-sm px-3 py-2 rounded-lg border hover:bg-gray-50"
|
||||
>
|
||||
Supabase test
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const text = input.trim();
|
||||
if (!text || status !== "ready") return;
|
||||
setInput("");
|
||||
void sendMessage({ text });
|
||||
}}
|
||||
className="flex gap-2"
|
||||
>
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
61
Les12-Tool-Calling/polderfest-demo/app/supabase/page.tsx
Normal file
61
Les12-Tool-Calling/polderfest-demo/app/supabase/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { supabaseAdmin } from "@/lib/supabase-admin";
|
||||
|
||||
type Band = {
|
||||
id: number;
|
||||
name: string;
|
||||
genre: string;
|
||||
stage: string;
|
||||
day: string;
|
||||
start_time: string;
|
||||
};
|
||||
|
||||
export default async function SupabasePage() {
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from("bands")
|
||||
.select("id,name,genre,stage,day,start_time")
|
||||
.order("popularity", { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
return (
|
||||
<main className="max-w-3xl mx-auto p-6 space-y-6">
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-2xl font-bold">Supabase verbinding</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Dit is een Server Component die met de service role key de top 10 bands
|
||||
uit de <code className="px-1 py-0.5 bg-gray-100 rounded">bands</code>{" "}
|
||||
tabel ophaalt.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{error ? (
|
||||
<div className="p-4 border border-red-200 bg-red-50 rounded-lg">
|
||||
<div className="font-medium text-red-800">
|
||||
Query faalde: {error.message}
|
||||
</div>
|
||||
<div className="text-sm text-red-700 mt-2">
|
||||
Check of de <code className="px-1 py-0.5 bg-white/70 rounded">bands</code>{" "}
|
||||
tabel bestaat en of je <code className="px-1 py-0.5 bg-white/70 rounded">SUPABASE_SERVICE_ROLE_KEY</code>{" "}
|
||||
klopt.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y border rounded-lg overflow-hidden">
|
||||
{(data as Band[] | null)?.map((b) => (
|
||||
<li key={b.id} className="p-4 flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold truncate">{b.name}</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{b.genre} • {b.stage}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-sm text-gray-700">
|
||||
{b.day} {b.start_time}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user