5afc7cd182
- Fixed a real i18n bug: the checked-count line called the wrong translation namespace and rendered the literal key on screen - Shopping lists can now be renamed and deleted from both the list index and detail pages (API already supported delete; rename was net new) - Root-caused "long list UI is off": meal-plan-generated lists never set an aisle, so every item fell into one undifferentiated "Other" bucket despite the grouping UI existing. Added a keyword-based aisle guesser wired into list generation (fallback only, never overrides an explicit aisle) plus a one-click "auto-categorize" for existing lists - Items can now be reordered by drag-and-drop within a category (dnd-kit), recategorized via a dropdown, deleted, searched, and sorted (category / alphabetical / unchecked-first); searching flattens the grouped view - Fixed a separate bug: AI-generated ingredients sometimes embedded the quantity/unit in the name itself (e.g. "2 cups flour" as one string). Added extractIngredientQuantity() as a Zod transform at both recipe create/update routes (the choke point every creation path funnels through) to split it back out, plus schema descriptions on the AI ingredient schemas as a prevention layer New migration 0028 (shopping_list_items.sort_order), left unapplied like the others. Verified with typecheck, lint, and a clean --no-cache docker build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import { generateObject } from "ai";
|
|
import { z } from "zod";
|
|
import { resolveModel, type AiConfig } from "../factory";
|
|
|
|
const ScaledIngredientsSchema = z.object({
|
|
ingredients: z.array(
|
|
z.object({
|
|
rawName: z.string().describe("Ingredient name only, e.g. 'flour' — never include the scaled quantity or unit here."),
|
|
quantity: z.string().describe("The scaled quantity as a number only, e.g. '3' or '1.5'."),
|
|
unit: z.string().nullable(),
|
|
note: z.string().optional(),
|
|
})
|
|
),
|
|
});
|
|
|
|
export type ScaledIngredient = z.infer<typeof ScaledIngredientsSchema>["ingredients"][number];
|
|
|
|
type RecipeInput = {
|
|
title: string;
|
|
baseServings: number;
|
|
ingredients: Array<{
|
|
rawName: string;
|
|
quantity: string | null;
|
|
unit: string | null;
|
|
}>;
|
|
};
|
|
|
|
export async function scaleRecipe(
|
|
recipe: RecipeInput,
|
|
targetServings: number,
|
|
originalServings: number,
|
|
config?: AiConfig,
|
|
locale?: string
|
|
): Promise<ScaledIngredient[]> {
|
|
const model = resolveModel(config);
|
|
|
|
const ingredientList = recipe.ingredients
|
|
.map((ing) => {
|
|
const parts = [ing.quantity, ing.unit, ing.rawName].filter(Boolean);
|
|
return `- ${parts.join(" ")}`;
|
|
})
|
|
.join("\n");
|
|
|
|
const { object } = await generateObject({
|
|
model,
|
|
schema: ScaledIngredientsSchema,
|
|
system: `You are a culinary scaling expert. Scale recipe ingredients from ${originalServings} to ${targetServings} servings. Use practical quantities — no 0.33 eggs (round to whole), no 0.17 cans (use 0.25 or 0.5), avoid impractical fractions. Substitute whole units where sensible. Add a note when rounding significantly.`,
|
|
prompt: `Recipe: "${recipe.title}"\n\nIngredients to scale:\n${ingredientList}`,
|
|
});
|
|
|
|
return object.ingredients;
|
|
}
|