From 3d04a148d614da4509345bfa96816a337111795f Mon Sep 17 00:00:00 2001 From: YummYume Date: Wed, 3 Jan 2024 00:33:23 +0100 Subject: [PATCH] Add load functions --- src/lib/components/Search.svelte | 41 ++++++++ src/lib/server/GPT.ts | 7 ++ src/routes/+page.svelte | 11 +-- src/routes/recipes/[slug]/+layout.server.ts | 38 +++++--- src/routes/recipes/[slug]/+page.server.ts | 9 +- src/routes/recipes/[slug]/+page.svelte | 2 +- .../[slug]/accompaniments/+page.server.ts | 23 ++++- .../[slug]/accompaniments/+page.svelte | 57 +++++++---- .../recipes/[slug]/similar/+page.server.ts | 43 ++++++++- .../recipes/[slug]/similar/+page.svelte | 81 ++++++++++------ src/routes/search/+page.server.ts | 96 +++++++++++++++++++ src/routes/search/+page.svelte | 69 +++++++++++-- 12 files changed, 397 insertions(+), 80 deletions(-) create mode 100644 src/lib/components/Search.svelte create mode 100644 src/lib/server/GPT.ts create mode 100644 src/routes/search/+page.server.ts diff --git a/src/lib/components/Search.svelte b/src/lib/components/Search.svelte new file mode 100644 index 0000000..0fe6f7b --- /dev/null +++ b/src/lib/components/Search.svelte @@ -0,0 +1,41 @@ + + +
{ + if (disabled) { + e.preventDefault(); + } + }} +> +
+
+
+ +

+ Votre recherche peut également contenir une demande, comme par exemple "Repas de Noël sans + gluten". +

+ +
+
diff --git a/src/lib/server/GPT.ts b/src/lib/server/GPT.ts new file mode 100644 index 0000000..25e610c --- /dev/null +++ b/src/lib/server/GPT.ts @@ -0,0 +1,7 @@ +import OpenAI from 'openai'; + +import { OPENAI_API_KEY } from '$env/static/private'; + +export const openai = new OpenAI({ + apiKey: OPENAI_API_KEY, +}); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e8a0c0d..3fb360e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,10 +1,9 @@ + +

CookConnect

- +
diff --git a/src/routes/recipes/[slug]/+layout.server.ts b/src/routes/recipes/[slug]/+layout.server.ts index d8839d4..a691878 100644 --- a/src/routes/recipes/[slug]/+layout.server.ts +++ b/src/routes/recipes/[slug]/+layout.server.ts @@ -1,23 +1,33 @@ +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; import { error } from '@sveltejs/kit'; -import { RECIPES } from '$lib/server/db'; +import { db } from '$lib/server/db'; import type { LayoutServerLoad } from './$types'; export const load = (async ({ params }) => { - const recipe = RECIPES.find((definedRecipe) => definedRecipe === params.slug); + try { + const recipe = await db.recipe.findUniqueOrThrow({ + where: { + slug: params.slug, + }, + }); - if (!recipe) { - error(404, "Cette recette n'existe pas."); - } + return { + recipe, + }; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === 'P2025') { + error(404, { + message: "Cette recette n'existe pas.", + }); + } + } + + // eslint-disable-next-line no-console + console.error('Error while loading recipe page:', e); - return { - recipe: { - dish: recipe, - slug: recipe, - ingredients: [], - steps: [], - shoppingList: [], - }, - }; + throw e; + } }) satisfies LayoutServerLoad; diff --git a/src/routes/recipes/[slug]/+page.server.ts b/src/routes/recipes/[slug]/+page.server.ts index 018d66b..68a1cd9 100644 --- a/src/routes/recipes/[slug]/+page.server.ts +++ b/src/routes/recipes/[slug]/+page.server.ts @@ -1,10 +1,17 @@ +import { jsonValueToArray } from '$lib/utils/json'; + import type { PageServerLoad } from './$types'; export const load = (async ({ parent }) => { const { recipe } = await parent(); return { - recipe, + recipe: { + ...recipe, + ingredients: jsonValueToArray(recipe.ingredients), + steps: jsonValueToArray(recipe.steps), + shoppingList: jsonValueToArray(recipe.shoppingList), + }, isFavourite: false, }; }) satisfies PageServerLoad; diff --git a/src/routes/recipes/[slug]/+page.svelte b/src/routes/recipes/[slug]/+page.svelte index ecaeed3..e287a9c 100644 --- a/src/routes/recipes/[slug]/+page.svelte +++ b/src/routes/recipes/[slug]/+page.svelte @@ -87,7 +87,7 @@
-

Description...

+

{data.recipe.description}

diff --git a/src/routes/recipes/[slug]/accompaniments/+page.server.ts b/src/routes/recipes/[slug]/accompaniments/+page.server.ts index 160e604..4f40c27 100644 --- a/src/routes/recipes/[slug]/accompaniments/+page.server.ts +++ b/src/routes/recipes/[slug]/accompaniments/+page.server.ts @@ -1,10 +1,31 @@ +import { openai } from '$lib/server/GPT'; +import { jsonValueToArray } from '$lib/utils/json'; + import type { PageServerLoad } from './$types'; export const load = (async ({ parent }) => { const { recipe } = await parent(); + const getAccompaniments = async () => { + const prompt = ` + Tu es Carlos, un assistant culinaire du site "CookConnect". + La recette actuellement consultée est "${recipe.dish}". + Ton travail consiste à me donner une liste d'accompagnements pour cette recette. + Je veux le résultat au format JSON, comme suit : ["accompagnement1", "accompagnement2", "accompagnement3"]. + Tu peux me donner au maximum 10 accompagnements. + `; + + const result = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [{ role: 'system', content: prompt }], + stream: false, + }); + + return jsonValueToArray(JSON.parse(result.choices[0].message.content ?? '')).slice(0, 10); + }; + return { + accompaniments: getAccompaniments(), recipe, - accompaniments: [] as string[], }; }) satisfies PageServerLoad; diff --git a/src/routes/recipes/[slug]/accompaniments/+page.svelte b/src/routes/recipes/[slug]/accompaniments/+page.svelte index f844d0f..3367ca0 100644 --- a/src/routes/recipes/[slug]/accompaniments/+page.svelte +++ b/src/routes/recipes/[slug]/accompaniments/+page.svelte @@ -1,7 +1,18 @@

{data.recipe.dish}

@@ -9,23 +20,33 @@

Accompagnements personnalisés

- {#if data.accompaniments.length > 0} -

- {data.accompaniments.length} accompagnement{data.accompaniments.length > 1 ? 's' : ''} trouvé{data - .accompaniments.length > 1 - ? 's' - : ''} pour "{data.recipe.dish}". + {#await data.accompaniments} + + {:then accompaniments} + {#if accompaniments.length > 0} +

+ {accompaniments.length} accompagnement{accompaniments.length > 1 ? 's' : ''} trouvé{accompaniments.length > + 1 + ? 's' + : ''} pour "{data.recipe.dish}". +

+
    + {#each accompaniments as accompaniment} +
  1. + {accompaniment} +
  2. + {/each} +
+ {:else} +

+ Aucun accompagnement trouvé pour "{data.recipe.dish}". +

+ {/if} + {:catch} +

+ Une erreur est survenue lors du chargement des accompagnements. Veuillez réessayer plus tard.

-
    - {#each data.accompaniments as accompaniment} -
  1. - {accompaniment} -
  2. - {/each} -
- {:else} -

- Aucun accompagnement trouvé pour "{data.recipe.dish}". -

- {/if} + {/await} + + Retour à la recette
diff --git a/src/routes/recipes/[slug]/similar/+page.server.ts b/src/routes/recipes/[slug]/similar/+page.server.ts index f33ca5b..98e84e7 100644 --- a/src/routes/recipes/[slug]/similar/+page.server.ts +++ b/src/routes/recipes/[slug]/similar/+page.server.ts @@ -1,11 +1,50 @@ +import { openai } from '$lib/server/GPT'; +import { db } from '$lib/server/db'; +import { jsonValueToArray } from '$lib/utils/json'; + import type { PageServerLoad } from './$types'; -import type { Recipe } from '@prisma/client'; export const load = (async ({ parent }) => { const { recipe } = await parent(); + const getSimilarRecipes = async () => { + const recipes = await db.recipe.findMany({ + select: { + dish: true, + slug: true, + description: true, + }, + where: { + slug: { + not: { + equals: recipe.slug, + }, + }, + }, + }); + + const prompt = ` + Tu es Carlos, un assistant culinaire du site "CookConnect". + La recette que tu consultes actuellement est "${recipe.dish}". + Je vais te donner une liste de recettes et ton travail est de me donner les recettes qui sont les plus adaptées pour être recommandées en tant que "recettes similaires". + Tu peux me donner entre 0 et 3 recettes. + Je veux le résultat au format JSON, comme suit : ["slug1", "slug2", "slug3"]. + Voici la liste des recettes : ${JSON.stringify(recipes)} + `; + + const result = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [{ role: 'system', content: prompt }], + stream: false, + }); + + const slugs = jsonValueToArray(JSON.parse(result.choices[0].message.content ?? '')); + + return recipes.filter((similarRecipe) => slugs.includes(similarRecipe.slug)).slice(0, 3); + }; + return { + similarRecipes: getSimilarRecipes(), recipe, - similarRecipes: [] as Recipe[], }; }) satisfies PageServerLoad; diff --git a/src/routes/recipes/[slug]/similar/+page.svelte b/src/routes/recipes/[slug]/similar/+page.svelte index bb0f1fe..e90dbc4 100644 --- a/src/routes/recipes/[slug]/similar/+page.svelte +++ b/src/routes/recipes/[slug]/similar/+page.svelte @@ -1,10 +1,20 @@

{data.recipe.dish}

@@ -12,35 +22,46 @@

Recettes similaires

- {#if data.similarRecipes.length > 0} -

- {data.similarRecipes.length} recette{data.similarRecipes.length > 1 ? 's' : ''} similaire{data - .similarRecipes.length > 1 - ? 's' - : ''} trouvée{data.similarRecipes.length > 1 ? 's' : ''} pour "{data.recipe.dish}". + {#await data.similarRecipes} + + {:then similarRecipes} + {#if similarRecipes.length > 0} +

+ {similarRecipes.length} recette{similarRecipes.length > 1 ? 's' : ''} similaire{similarRecipes.length > + 1 + ? 's' + : ''} trouvée{similarRecipes.length > 1 ? 's' : ''} pour "{data.recipe.dish}". +

+ + {:else} +

+ Aucune recette similaire trouvée pour "{data.recipe.dish}". +

+ {/if} + {:catch} +

+ Une erreur est survenue lors du chargement des recettes similaires. Veuillez réessayer plus + tard.

- - {:else} -

- Aucune recette similaire trouvée pour "{data.recipe.dish}". -

- {/if} + {/await} + + Retour à la recette
diff --git a/src/routes/search/+page.server.ts b/src/routes/search/+page.server.ts new file mode 100644 index 0000000..b5c9ed3 --- /dev/null +++ b/src/routes/search/+page.server.ts @@ -0,0 +1,96 @@ +import { openai } from '$lib/server/GPT'; +import { db } from '$lib/server/db'; + +import type { PageServerLoad } from './$types'; + +type Dish = { + dish: string; + slug: string; +}; + +export const load = (async ({ url }) => { + const query = url.searchParams.get('q') ?? ''; + + const getResult = async (): Promise<{ recipe: null | Dish; suggestions: Dish[] }> => { + const recipes = await db.recipe.findMany({ + select: { + dish: true, + slug: true, + description: true, + }, + }); + + if (recipes.length === 0 || !query) { + return { + recipe: null, + suggestions: [], + }; + } + + const output = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + stream: false, + messages: [ + { + role: 'system', + content: ` + TU NE DOIS RETOURNER QUE DU JSON. + À partir de maintenant, tu es un assistant de cuisine personnel. + Ceci est la liste de recettes de cuisine dont tu disposes, au format JSON : ${JSON.stringify( + recipes, + )}. + Ton but est de chercher dans la liste de recettes de cuisine une recette qui correspond à la demande de l'utilisateur parmi les recettes que tu as. Tu dois te contenter de chercher dans la liste de recettes que tu as et ne peux pas en inventer de nouvelles. + L'utilisateur peut te demander le nom d'un plat mais aussi le nom d'un ingrédient, une description de ce qu'il souhaite manger, ou une demande pour une occasion spéciale. Il peut également te demander une recette aléatoire. + Tu peux ne pas trouver de recette, ce n'est pas grave, retourne {"recipe": null, "suggestions": []} et ignore de la demande de l'utilisateur. + Si tu juges que la demande de l'utilisateur n'est pas valide, est obscène, insultante, ou ne correspond pas à une recette de cuisine, retourne {"recipe": null, "suggestions": []} et ignore la demande. + Que tu trouves une recette ou non, tu peux également donner des suggestions (entre 0 et 10) de recettes qui pourraient répondre à la demande de l'utilisateur. Les suggestions doivent être des recettes qui ont un rapport avec la demande de l'utilisateur ou qui ressemblent à la recette que tu as trouvée (si tu en as trouvée une). + Tu peux ne pas donner de suggestions si tu n'en trouves pas. + Retourne le résultat au format JSON suivant : + { + "recipe": {"dish": string, "slug": slug}|null, + "suggestions": {"dish": string, "slug": slug}[], + } + `, + }, + { + role: 'user', + content: query, + }, + ], + }); + try { + const result: { recipe: Dish | null; suggestions: Dish[] } = JSON.parse( + output.choices[0].message.content ?? '', + ); + + // Remove non-existing recipes + result.recipe = recipes.find((r) => r.slug === result.recipe?.slug) ?? null; + result.suggestions = result.suggestions.reduce((suggestions, suggestion) => { + const recipe = recipes.find((r) => r.slug === suggestion.slug); + + if ( + recipe && + recipe.slug !== result.recipe?.slug && + !suggestions.some((r) => r.slug === recipe.slug) && + suggestions.length < 10 + ) { + suggestions.push(recipe); + } + + return suggestions; + }, []); + + return result; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error searching for recipe:', e); + + throw e; + } + }; + + return { + result: getResult(), + query, + }; +}) satisfies PageServerLoad; diff --git a/src/routes/search/+page.svelte b/src/routes/search/+page.svelte index 51a1c56..0a759d3 100644 --- a/src/routes/search/+page.svelte +++ b/src/routes/search/+page.svelte @@ -1,18 +1,73 @@

Recherche

- +
- + {#await data.result} + + {:then value} + {#if !value.recipe} + + {#if data.query.trim() !== ''} +

Aucun résulat pour "{data.query}".

+
+ + +
+ {:else} +

+ Il va être compliqué de trouver une recette sans nom. +

+ {/if} +
+ {:else} +

Recette trouvée.

+ + {value.recipe.dish} + + {/if} + + {#if value.suggestions.length} + +

Suggestions

+ + +
+ {/if} + {:catch} +

+ Oups! Quelque chose s'est mal passé. Veuillez réessayer plus tard. +

+ {/await}