Terug naar overzicht
    AI-toepassingen

    AI-powered menu generation: van user prompt naar structured output in 3 minuten

    Door Ron Hoskens
    6 november 2025
    7 min leestijd

    AI-Powered Menu Generation: Van User Prompt naar Structured Output in 3 Minuten

    Het Probleem: Menu Samenstellen Kost Belachelijk Veel Tijd

    Restaurant eigenaren kennen de pijn:

    • πŸ• 2 uur staren naar recepten
    • πŸ“ Eindeloos wikken en wegen tussen gangen
    • 🧠 Decision fatigue al voordat je eerste gast binnenkomt

    Of je nu een kerstdiner plant voor 40 personen of een 21-diner voor jezelf, het is hetzelfde verhaal: te veel opties, te weinig structuur.

    De Oplossing: Structured AI Output in 180 Seconden

    We bouwden een systeem dat:

    1. βœ… Begrijpt wat je wilt (natural language input)
    2. βœ… Genereert complete menu's (AI-powered)
    3. βœ… Levert gestructureerde data (geen rommelende tekstblobs)
    4. βœ… Werkt in 3 minuten (inclusief verfijnen)

    Tech Stack:

    • Frontend: React + TypeScript
    • Backend: Supabase Edge Functions
    • AI: Lovable AI (Gemini 2.5 Flash)
    • Type Safety: Zod schemas

    Laten we onder de motorkap duiken πŸ”§


    Architectuur: Van Input naar Structured Menu

    Flow Diagram

    User Input (dietary preferences, allergies, etc.)
        ↓
    React Form Validation
        ↓
    Supabase Edge Function
        ↓
    Lovable AI Gateway (Gemini 2.5 Flash)
        ↓
    Tool Calling (Zod Schema Validation)
        ↓
    Structured JSON Output
        ↓
    Frontend Rendering + PDF Export
    

    Stap 1: Type-Safe User Input

    Zod Schema voor Preferences

    // src/lib/schemas/menuPreferences.ts
    import { z } from "zod";
    
    export const menuPreferencesSchema = z.object({
      dietaryRestrictions: z.array(z.string()).default([]),
      allergies: z.array(z.string()).default([]),
      cuisinePreference: z.string().optional(),
      spicyLevel: z.enum(["mild", "medium", "hot"]).default("medium"),
      alcoholPreference: z.enum(["with", "without", "optional"]).default("optional"),
      courses: z.number().min(3).max(7).default(4),
    });
    
    export type MenuPreferences = z.infer<typeof menuPreferencesSchema>;
    

    Waarom Zod?

    • βœ… Runtime validation (catch bugs vroeg)
    • βœ… TypeScript types worden automatisch gegenereerd
    • βœ… Developer experience: autocomplete + type checking

    Stap 2: Edge Function met AI Tool Calling

    Prompt Engineering voor Tool Calling

    // supabase/functions/generate-menu/index.ts
    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
    };
    
    serve(async (req) => {
      if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
    
      try {
        const { preferences, theme } = await req.json();
        const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY");
    
        // System prompt based on theme (kerst vs 21-diner)
        const systemPrompt = theme === "christmas" 
          ? "Je bent een AI chef gespecialiseerd in feestelijke kerstdiners..."
          : "Je bent een AI chef gespecialiseerd in 21-diner menu samenstelling...";
    
        const response = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${LOVABLE_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            model: "google/gemini-2.5-flash",
            messages: [
              { role: "system", content: systemPrompt },
              { role: "user", content: `Maak een menu met: ${JSON.stringify(preferences)}` }
            ],
            tools: [
              {
                type: "function",
                function: {
                  name: "create_menu",
                  description: "Genereer een gestructureerd restaurant menu",
                  parameters: {
                    type: "object",
                    properties: {
                      menu: {
                        type: "object",
                        properties: {
                          title: { type: "string" },
                          courses: {
                            type: "array",
                            items: {
                              type: "object",
                              properties: {
                                name: { type: "string" },
                                dish_name: { type: "string" },
                                description: { type: "string" },
                                pairing: { 
                                  type: "object",
                                  properties: {
                                    drink: { type: "string" },
                                    why: { type: "string" }
                                  }
                                }
                              },
                              required: ["name", "dish_name", "description", "pairing"]
                            }
                          }
                        },
                        required: ["title", "courses"]
                      }
                    },
                    required: ["menu"]
                  }
                }
              }
            ],
            tool_choice: { type: "function", function: { name: "create_menu" } }
          }),
        });
    
        const data = await response.json();
        const toolCall = data.choices[0].message.tool_calls[0];
        const menuData = JSON.parse(toolCall.function.arguments);
    
        return new Response(
          JSON.stringify({ success: true, menu: menuData.menu }),
          { headers: { ...corsHeaders, "Content-Type": "application/json" } }
        );
    
      } catch (error) {
        console.error("Error generating menu:", error);
        return new Response(
          JSON.stringify({ success: false, error: error.message }),
          { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
        );
      }
    });
    

    Stap 3: Frontend Integration

    React Hook voor Menu Generation

    // src/hooks/useMenuGeneration.ts
    import { useState } from "react";
    import { supabase } from "@/integrations/supabase/client";
    import { useToast } from "@/hooks/use-toast";
    import type { MenuPreferences } from "@/lib/schemas/menuPreferences";
    
    export function useMenuGeneration(theme: "christmas" | "casual") {
      const [isLoading, setIsLoading] = useState(false);
      const [menu, setMenu] = useState(null);
      const { toast } = useToast();
    
      const generateMenu = async (preferences: MenuPreferences) => {
        setIsLoading(true);
    
        try {
          const { data, error } = await supabase.functions.invoke("generate-menu", {
            body: { preferences, theme }
          });
    
          if (error) throw error;
    
          setMenu(data.menu);
          toast({
            title: "βœ… Menu gegenereerd!",
            description: "Je menu is klaar om te bekijken.",
          });
    
        } catch (error) {
          toast({
            title: "❌ Fout",
            description: "Er ging iets mis. Probeer opnieuw.",
            variant: "destructive"
          });
        } finally {
          setIsLoading(false);
        }
      };
    
      return { generateMenu, menu, isLoading };
    }
    

    Waarom Tool Calling > Prompt Hacking?

    Zonder Tool Calling (oude methode):

    // ❌ Fragile prompt engineering
    const prompt = `
    Genereer een JSON menu in dit formaat:
    {
      "title": "...",
      "courses": [...]
    }
    
    BELANGRIJK: Output ALLEEN valid JSON, geen extra tekst!
    `;
    
    // Problemen:
    // - Model voegt soms "Hier is je menu:" toe vΓ³Γ³r JSON
    // - JSON formatting errors (trailing commas, quotes)
    // - Geen schema validatie β†’ runtime crashes
    

    Met Tool Calling (nieuwe methode):

    // βœ… Gegarandeerd structured output
    tools: [{
      function: {
        parameters: {
          type: "object",
          properties: { /* exact schema */ }
        }
      }
    }]
    
    // Voordelen:
    // βœ… Model MOET aan schema voldoen
    // βœ… Geen parsing errors
    // βœ… Type-safe vanaf AI tot frontend
    

    Performance: Hoe Halen We 3 Minuten?

    Bottleneck Analyse

    | Stap | Tijd | Optimalisatie | |------|------|--------------| | User input | 30s | Autocomplete + defaults | | API call | 5-8s | Gemini Flash (snelste model) | | Rendering | < 1s | React virtualization | | Refinement | 60-90s | Partial regeneration |

    Total: ~3 minuten inclusief user tijd.

    Model Keuze: Gemini Flash vs Pro

    // Gemini 2.5 Flash (onze keuze)
    // - Latency: 5-8 seconden voor 4-gangen menu
    // - Cost: €0.02 per menu
    // - Quality: Perfect voor gestructureerde output
    
    // Gemini 2.5 Pro (overkill)
    // - Latency: 12-15 seconden
    // - Cost: €0.08 per menu  
    // - Quality: Marginaal beter, niet waard voor use case
    

    Conclusie: Flash is de sweet spot voor menu generation.


    Lessons Learned: 3 Maanden Productie

    1. Dietary Restrictions Are Complex

    Eerste versie:

    dietaryRestrictions: z.enum(["vegetarian", "vegan", "gluten-free"])
    

    Realiteit:

    dietaryRestrictions: z.array(z.string()).default([])
    // Users combineren: "vegan + gluten-free + low-sodium"
    

    Fix: Multi-select met custom input field.

    2. Alcohol is Culturally Sensitive

    // ❌ Assumed everyone wants wine pairing
    pairing: { type: "wine", name: "..." }
    
    // βœ… Added user preference
    alcoholPreference: z.enum(["with", "without", "optional"])
    // Model suggests tea/juice alternatives if "without"
    

    3. Menu Length Variability

    // ❌ Fixed 4 courses
    courses: z.array(courseSchema).length(4)
    
    // βœ… User-defined range
    courses: z.number().min(3).max(7).default(4)
    // Christmas dinners often want 5-7 courses
    

    SEO & Discovery: Hoe Vinden Klanten Ons?

    Primary Keywords

    • "kerstmenu generator AI"
    • "21 diner menu samenstellen"
    • "restaurant menu maken met AI"

    Content Strategy

    1. Blog posts (like this one) β†’ SEO voor educational queries
    2. Tool pages (/kerst, /21-diner) β†’ Conversion optimized
    3. Case studies β†’ Social proof + backlinks

    Result: 60% organic traffic binnen 8 weken na launch.


    Conclusie: Structured AI is The Future

    Wat we bouwden:

    • βœ… Type-safe AI integration
    • βœ… Sub-10 second generation
    • βœ… Foolproof structured output
    • βœ… Production-ready error handling

    Waarom dit schaalt:

    • πŸ”„ Zelfde architectuur voor alle seizoenen (Pasen, BBQ, Valentijn)
    • 🌍 Easy internationalization (Engels, Frans support)
    • πŸ“Š A/B testing via prompt variations

    Probeer Het Zelf

    Klaar om in 3 minuten je eigen menu te genereren?

    Of bouw je eigen versie:
    Volledige source code + architectuur breakdown beschikbaar in ons GitHub repo.


    Questions? Stuur me een DM op LinkedIn of check onze andere blog posts voor meer AI implementation deep-dives.

    Wil je weten hoe deze AI-oplossing voor jouw bedrijf werkt?

    #AI#Automatisering#21-diner#Culinair#Productiviteit#Web Apps

    Van inzicht naar toepassing

    De praktijkcase: een compleet diner plannen met AI

    Klaar om slimmer te werken met AI?

    Je hoeft niet groot te beginnen. EΓ©n simpele taak kan binnen 48 uur live zijn, gerekend vanaf de kick-off waarin we de workflow samen vastleggen.