From 7422ecad58793e62525553a9f50225882dd28543 Mon Sep 17 00:00:00 2001 From: Dev_id Date: Fri, 27 Mar 2026 16:43:40 +0000 Subject: [PATCH 1/6] feat: implement cookie management for recipe sessions and enhance recipe saving logic --- app/actions/recipe_action.ts | 40 ++++++++++++++++- app/actions/retrieve_recipe_action.ts | 17 ++++++- app/api/recipe/route.ts | 64 ++++++++++++++++++--------- app/lib/cookies.ts | 33 +++++++++++--- app/lib/helpers.ts | 4 +- 5 files changed, 128 insertions(+), 30 deletions(-) diff --git a/app/actions/recipe_action.ts b/app/actions/recipe_action.ts index 76ef63f..199fa2e 100644 --- a/app/actions/recipe_action.ts +++ b/app/actions/recipe_action.ts @@ -1,3 +1,4 @@ +import { toast } from "@/hooks/use-toast"; import axios from "axios"; interface RecipeActionProps { @@ -20,6 +21,8 @@ export default async function RecipeAction(props: RecipeActionProps): Promise convo.id === id); if (!convoHistories.length) { + // return the local recipe if it exists, otherwise return an error + const localRecipe = await getCookie(id); + if (localRecipe) { + return { + success: true, + data: { + ...localRecipe?.recipe, + imgSrc: localRecipe?.imgSrc || null, + } as HistoryItemData + } + } + return { success: false, - error: "No conversation found with that ID", + error: "No recipe found with that ID", } } @@ -33,5 +47,4 @@ export default async function ConvoDetails( {id}: {id: string} ) { success: true, data: convoHistories[0].data, } - } \ No newline at end of file diff --git a/app/api/recipe/route.ts b/app/api/recipe/route.ts index c2c3776..d41b634 100644 --- a/app/api/recipe/route.ts +++ b/app/api/recipe/route.ts @@ -4,7 +4,7 @@ import { cleanJSON, getCachedImage, getUserID } from "@/lib/helpers"; import { randomUUID } from "crypto"; import { addSeshHistory } from "@/lib/session"; import { imageURLFromID } from "@/lib/utils"; -import { waitUntil } from '@vercel/functions'; +import { deleteCookie, getCookie, saveAsCookie } from "@/app/lib/cookies"; export async function POST(req: NextRequest) { @@ -16,29 +16,26 @@ export async function POST(req: NextRequest) { const prevGenerated = { mealName, ingredients }; - waitUntil( - (async () => { - try { - const [userID, chatHistory] = await Promise.all([ - getUserID(), - convoHistory(imgSrc, JSON.stringify(prevGenerated)) - ]); + try { + const chatHistory = await convoHistory(imgSrc, JSON.stringify(prevGenerated)); - // Call AI - const ai_response = await getRecipe(details, chatHistory); - const recipe = cleanJSON(ai_response); + // Call AI + const ai_response = await getRecipe(details, chatHistory); + const recipe = cleanJSON(ai_response); - // Save to Database - await addSeshHistory(userID, convoID, recipe, imgSrc); + // Save temporarily + await saveAsCookie( + convoID, { + recipe, + imgSrc, + }); - console.log(`✅ Successfully generated and saved recipe for ${convoID}`); + console.log(`✅ Successfully generated and saved recipe for ${convoID}`); - } catch (bgError) { - // Wont reach FE - console.error(`❌ Background processing failed for ${convoID}:`, bgError); - } - })() - ); + } catch (bgError) { + // Wont reach FE + console.error(`❌ Background processing failed for ${convoID}:`, bgError); + } return NextResponse.json({ success: true, @@ -47,6 +44,33 @@ export async function POST(req: NextRequest) { } +// USE A PUT to save the session to DB +export async function PUT(req: NextRequest) { + const body = await req.json(); + const { convoID } = body; + + try { + const [userID, storedData] = await Promise.all([ + getUserID(), + getCookie(convoID) + ]); + + if (!storedData) { // it has already been saved / issue, return moved + return NextResponse.json({ success: false, error: "No session data found" }, { status: 302 }); + } + + await addSeshHistory(userID, convoID, storedData.recipe, storedData.imgSrc); + console.log(`✅ Successfully added ${convoID} to DB`); + + deleteCookie(convoID); // clean up the cookie after saving to DB + + return NextResponse.json({ success: true }, { status: 200 }); + + } catch (err) { + console.error(`❌ Failed to add ${convoID} to DB:`, err); + return NextResponse.json({ success: false, error: "Failed to save session" }, { status: 500 }); + } +} diff --git a/app/lib/cookies.ts b/app/lib/cookies.ts index ed3a565..038f1d1 100644 --- a/app/lib/cookies.ts +++ b/app/lib/cookies.ts @@ -3,20 +3,43 @@ import { UUID } from "crypto"; import { cookies } from "next/headers"; + +export async function getCookie(key: string) { + const cookieJar = await cookies(); + const cookie = cookieJar.get(key); + + return cookie? JSON.parse(cookie.value) : null +} + + export async function setCookies(data: {userID: UUID}) { const cookieJar = await cookies(); cookieJar.set("session_data", JSON.stringify(data), { httpOnly: true, secure: process.env.NODE_ENV === "production", - maxAge: 60 * 60 * 24, // 1 day + maxAge: 60 * 60 * 24 * 7, // 1 week path: "/", }); } -export async function getCookies(){ - const cookieJar = await cookies(); - const cookie = cookieJar.get("session_data"); +export async function getSessionCookies(){ + const sessionData = await getCookie("session_data"); + return sessionData || {}; +} - return cookie? JSON.parse(cookie.value) : {} +export async function saveAsCookie(key: string, data: Record) { + const cookieJar = await cookies(); + cookieJar.set(key, JSON.stringify(data), { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + maxAge: 60 * 60 * 24, // 1 day + path: "/", + }); } + + +export async function deleteCookie(key: string) { + const cookieJar = await cookies(); + cookieJar.delete(key); +} \ No newline at end of file diff --git a/app/lib/helpers.ts b/app/lib/helpers.ts index 9a4a76d..81fe1b9 100644 --- a/app/lib/helpers.ts +++ b/app/lib/helpers.ts @@ -1,5 +1,5 @@ import { randomUUID } from "crypto"; -import { getCookies, setCookies } from "./cookies"; +import { getSessionCookies, setCookies } from "./cookies"; import { cache } from "react"; @@ -24,7 +24,7 @@ export function cleanJSON(content: string) { export async function getUserID(){ // Retrieve or set session cookie - const cookies = await getCookies(); + const cookies = await getSessionCookies(); let userID; if (cookies.userID) { From 0cbaa9c33759f98c5633055e77bba7963ee891cd Mon Sep 17 00:00:00 2001 From: Dev_id Date: Fri, 27 Mar 2026 16:46:15 +0000 Subject: [PATCH 2/6] fix: lint errors --- app/actions/recipe_action.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/actions/recipe_action.ts b/app/actions/recipe_action.ts index 199fa2e..56f635b 100644 --- a/app/actions/recipe_action.ts +++ b/app/actions/recipe_action.ts @@ -1,4 +1,3 @@ -import { toast } from "@/hooks/use-toast"; import axios from "axios"; interface RecipeActionProps { From 0b03637ff332d8e43480321db7762c8bb756effb Mon Sep 17 00:00:00 2001 From: Dev_id Date: Fri, 27 Mar 2026 17:09:08 +0000 Subject: [PATCH 3/6] chore: add logs --- app/actions/recipe_action.ts | 2 ++ app/api/recipe/route.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/actions/recipe_action.ts b/app/actions/recipe_action.ts index 56f635b..bb0df46 100644 --- a/app/actions/recipe_action.ts +++ b/app/actions/recipe_action.ts @@ -15,11 +15,13 @@ interface RecipeResponse { export default async function RecipeAction(props: RecipeActionProps): Promise { try { + console.log("Initiating recipe generation with props:", props); const { data } = await axios.post( "/api/recipe", props ); + console.log("try to save recipe to DB with convoID:", data.id); ensureRecipeSaved(data.id!); return data; diff --git a/app/api/recipe/route.ts b/app/api/recipe/route.ts index d41b634..0392606 100644 --- a/app/api/recipe/route.ts +++ b/app/api/recipe/route.ts @@ -15,13 +15,16 @@ export async function POST(req: NextRequest) { const imgSrc = imgID ? imageURLFromID(imgID) : null; const prevGenerated = { mealName, ingredients }; + console.log(`Received request for ${convoID} with meal: ${mealName} and image: ${imgSrc}`); try { const chatHistory = await convoHistory(imgSrc, JSON.stringify(prevGenerated)); + console.log("got convo history") // Call AI const ai_response = await getRecipe(details, chatHistory); const recipe = cleanJSON(ai_response); + console.log("got recipe from AI") // Save temporarily await saveAsCookie( From 15606a371b3a9fc94a4578b4cbdf2bb6167c8418 Mon Sep 17 00:00:00 2001 From: Dev_id Date: Fri, 27 Mar 2026 17:14:30 +0000 Subject: [PATCH 4/6] update model for recipe generation --- app/lib/recipe.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/recipe.js b/app/lib/recipe.js index 1c62a65..44b8621 100644 --- a/app/lib/recipe.js +++ b/app/lib/recipe.js @@ -8,7 +8,7 @@ const ai = new GoogleGenAI({ }); export async function getRecipe(description, history) { - const model = 'gemini-2.5-pro'; + const model = 'gemini-3.1-pro-preview'; // Create the chat session const chat = ai.chats.create({ From 676f72577d99d3241150ef9ad9061819a8eb822a Mon Sep 17 00:00:00 2001 From: Dev_id Date: Fri, 27 Mar 2026 17:30:11 +0000 Subject: [PATCH 5/6] update prompt for recipe generation --- app/lib/recipe.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/recipe.js b/app/lib/recipe.js index 44b8621..ef5fb87 100644 --- a/app/lib/recipe.js +++ b/app/lib/recipe.js @@ -24,7 +24,7 @@ export async function getRecipe(description, history) { type Step: {description, duration, price?} # price only when necessary (e.g., if a budget was mentioned) Requirements: - 1. Use Google Search to find special ingredients and current, accurate prices. + 1. Try your best to assume special ingredients and accurate prices. 2. Respond strictly in the format: { "mealName": "name", "ingredients": Array, "recipe": Array }`; // Using stream for better UX, or you can use chat.sendMessage for a single block From 08f20e78ea826921280661f2c13a61f151671707 Mon Sep 17 00:00:00 2001 From: Dev_id Date: Fri, 27 Mar 2026 17:34:25 +0000 Subject: [PATCH 6/6] update prompt for recipe generation --- app/lib/recipe.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/lib/recipe.js b/app/lib/recipe.js index ef5fb87..50c11cf 100644 --- a/app/lib/recipe.js +++ b/app/lib/recipe.js @@ -24,8 +24,7 @@ export async function getRecipe(description, history) { type Step: {description, duration, price?} # price only when necessary (e.g., if a budget was mentioned) Requirements: - 1. Try your best to assume special ingredients and accurate prices. - 2. Respond strictly in the format: { "mealName": "name", "ingredients": Array, "recipe": Array }`; + . Respond strictly in the format: { "mealName": "name", "ingredients": Array, "recipe": Array }`; // Using stream for better UX, or you can use chat.sendMessage for a single block const result = await chat.sendMessage({ message: prompt });