Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion app/actions/recipe_action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ interface RecipeResponse {

export default async function RecipeAction(props: RecipeActionProps): Promise<RecipeResponse> {
try {
console.log("Initiating recipe generation with props:", props);
const { data } = await axios.post<RecipeResponse>(
"/api/recipe",
props
);

console.log("try to save recipe to DB with convoID:", data.id);
ensureRecipeSaved(data.id!);

return data;

} catch (err) {
Expand All @@ -36,4 +40,39 @@ export default async function RecipeAction(props: RecipeActionProps): Promise<Re
error: "Failed to connect to the server"
};
}
}
}


async function ensureRecipeSaved(convoID: string) {
const maxRetries = 5;
const retryDelay = 2000; // 2 seconds

let attempts = 0;

async function run() {
try {
const response = await axios.put("/api/recipe", { convoID });

if (response.data.success) {
console.log(`✅ Recipe with convoID ${convoID} successfully saved to DB.`);
return;
} else {
console.warn(`⚠️ Attempt ${attempts + 1}: Server responded with an error: ${response.data.error}`);
}
} catch (err) {
if (axios.isAxiosError(err)) {
console.warn(`⚠️ Attempt ${attempts + 1}: Failed to save recipe with convoID ${convoID}. Error: ${err.message}`);
} else {
console.warn(`⚠️ Attempt ${attempts + 1}: An unexpected error occurred while saving recipe with convoID ${convoID}.`);
}
}

attempts++;
if (attempts < maxRetries) {
setTimeout(run, retryDelay);
} else {
console.error(`❌ Failed to save recipe with convoID ${convoID} after ${maxRetries} attempts.`);
}
}
run();
}
17 changes: 15 additions & 2 deletions app/actions/retrieve_recipe_action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use server";

import { getCookie } from "../lib/cookies";
import { HistoryItemData } from "../lib/definitions";
import { getUserID } from "../lib/helpers";
import { getSeshHistory } from "../lib/session";

Expand All @@ -23,15 +25,26 @@ export default async function ConvoDetails( {id}: {id: string} ) {
const convoHistories = fullHistory.filter((convo) => 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",
}
}

return {
success: true,
data: convoHistories[0].data,
}

}
67 changes: 47 additions & 20 deletions app/api/recipe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -15,30 +15,30 @@ 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}`);

waitUntil(
(async () => {
try {
const [userID, chatHistory] = await Promise.all([
getUserID(),
convoHistory(imgSrc, JSON.stringify(prevGenerated))
]);
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);
// Call AI
const ai_response = await getRecipe(details, chatHistory);
const recipe = cleanJSON(ai_response);
console.log("got recipe from AI")

// 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,
Expand All @@ -47,6 +47,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 });
}
}



Expand Down
33 changes: 28 additions & 5 deletions app/lib/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) {
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);
}
4 changes: 2 additions & 2 deletions app/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { randomUUID } from "crypto";
import { getCookies, setCookies } from "./cookies";
import { getSessionCookies, setCookies } from "./cookies";
import { cache } from "react";


Expand All @@ -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) {
Expand Down
5 changes: 2 additions & 3 deletions app/lib/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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. Use Google Search to find special ingredients and current, accurate prices.
2. Respond strictly in the format: { "mealName": "name", "ingredients": Array<Ingredient>, "recipe": Array<Step> }`;
. Respond strictly in the format: { "mealName": "name", "ingredients": Array<Ingredient>, "recipe": Array<Step> }`;

// Using stream for better UX, or you can use chat.sendMessage for a single block
const result = await chat.sendMessage({ message: prompt });
Expand Down