V1.1 met AI functie
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
const db = require("../config/database");
|
||||
const ProductRepository = require("../repositories/ProductRepository");
|
||||
|
||||
|
||||
function normalizeName(name) {
|
||||
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
|
||||
class MenuService {
|
||||
|
||||
|
||||
async generate(preferences) {
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("OPENAI_API_KEY ontbreekt in de configuratie.");
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.OPENAI_MODEL || "gpt-4o-mini",
|
||||
response_format: { type: "json_object" },
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "Je maakt praktische weekmenu's. Geef uitsluitend geldige JSON terug met precies de structuur {days:[{day,recipe,ingredients:[{name,amount,unit}]}]}. Gebruik zeven dagen, Nederlandse productnamen, positieve numerieke hoeveelheden en korte recepten."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Maak een weekmenu voor ${preferences.people} personen. Dieet: ${preferences.diet || "geen specifiek dieet"}. Niet gewenst: ${preferences.exclude || "niets"}. Budget: ${preferences.budget || "geen voorkeur"}.`
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let providerMessage = "onbekende fout";
|
||||
try {
|
||||
const errorPayload = await response.json();
|
||||
providerMessage = errorPayload.error?.message || providerMessage;
|
||||
} catch (error) {
|
||||
providerMessage = `HTTP ${response.status}`;
|
||||
}
|
||||
throw new Error(`AI-service fout (${response.status}): ${providerMessage}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!payload.choices?.[0]?.message?.content) {
|
||||
throw new Error("De AI-service gaf geen menu-inhoud terug.");
|
||||
}
|
||||
let menu;
|
||||
try {
|
||||
menu = JSON.parse(payload.choices?.[0]?.message?.content || "{}");
|
||||
} catch (error) {
|
||||
throw new Error("De AI-service gaf geen geldig menu terug.");
|
||||
}
|
||||
|
||||
if (!Array.isArray(menu.days) || menu.days.length === 0) {
|
||||
throw new Error("De AI-service gaf een leeg menu terug.");
|
||||
}
|
||||
|
||||
menu.days = menu.days.map(day => ({
|
||||
day: String(day.day || "Dag"),
|
||||
recipe: String(day.recipe || "Onbekend recept"),
|
||||
ingredients: Array.isArray(day.ingredients)
|
||||
? day.ingredients
|
||||
.filter(item => item && String(item.name || "").trim())
|
||||
.map(item => ({
|
||||
name: String(item.name).trim(),
|
||||
amount: Number.isFinite(Number(item.amount)) ? Number(item.amount) : 1,
|
||||
unit: String(item.unit || "")
|
||||
}))
|
||||
: []
|
||||
}));
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
|
||||
getNewProducts(menu) {
|
||||
const existingNames = new Set(
|
||||
ProductRepository.getAll().map(product => normalizeName(product.name))
|
||||
);
|
||||
const seen = new Set();
|
||||
const newProducts = [];
|
||||
|
||||
for (const day of menu.days) {
|
||||
for (const ingredient of day.ingredients) {
|
||||
const key = normalizeName(ingredient.name);
|
||||
if (!existingNames.has(key) && !seen.has(key)) {
|
||||
seen.add(key);
|
||||
newProducts.push(ingredient.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newProducts;
|
||||
}
|
||||
|
||||
|
||||
addToShoppingList(householdId, name, menu) {
|
||||
const products = ProductRepository.getAll();
|
||||
const productsByName = new Map(
|
||||
products.map(product => [normalizeName(product.name), product])
|
||||
);
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (name, category_id, barcode) VALUES (?, NULL, NULL)
|
||||
`);
|
||||
const insertList = db.prepare(`
|
||||
INSERT INTO shopping_lists (household_id, name) VALUES (?, ?)
|
||||
`);
|
||||
const insertItem = db.prepare(`
|
||||
INSERT INTO shopping_list_items (list_id, product_id, amount, sort_order)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const create = db.transaction(() => {
|
||||
const listResult = insertList.run(householdId, name);
|
||||
const listId = listResult.lastInsertRowid;
|
||||
let sortOrder = 0;
|
||||
const added = new Set();
|
||||
|
||||
for (const day of menu.days) {
|
||||
for (const ingredient of day.ingredients) {
|
||||
const key = normalizeName(ingredient.name);
|
||||
if (added.has(key)) continue;
|
||||
|
||||
let product = productsByName.get(key);
|
||||
if (!product) {
|
||||
const result = insertProduct.run(ingredient.name);
|
||||
product = { id: result.lastInsertRowid, name: ingredient.name };
|
||||
productsByName.set(key, product);
|
||||
}
|
||||
|
||||
const amount = Number(ingredient.amount);
|
||||
insertItem.run(listId, product.id, amount > 0 ? amount : 1, sortOrder);
|
||||
added.add(key);
|
||||
sortOrder += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return listId;
|
||||
});
|
||||
|
||||
return create();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
module.exports = new MenuService();
|
||||
Reference in New Issue
Block a user