feat: shopping list rename/delete, item reorder+categories+search, ingredient-quantity parsing fix
- 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>
This commit is contained in:
@@ -10,9 +10,9 @@ const MealPlanSchema = z.object({
|
||||
title: z.string().max(150),
|
||||
description: z.string().max(300),
|
||||
ingredients: z.array(z.object({
|
||||
rawName: z.string(),
|
||||
quantity: z.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
rawName: z.string().describe("Ingredient name only, e.g. 'flour' — never include the quantity or unit here."),
|
||||
quantity: z.number().optional().describe("A number only — never combined with the unit."),
|
||||
unit: z.string().optional().describe("The unit only, e.g. 'cup', 'g' — never combined with the quantity or name."),
|
||||
})).max(20),
|
||||
steps: z.array(z.object({
|
||||
instruction: z.string(),
|
||||
|
||||
@@ -17,9 +17,13 @@ export const stepSchema = z.object({
|
||||
|
||||
export function ingredientSchema<Q extends z.ZodTypeAny>(quantity: Q) {
|
||||
return z.object({
|
||||
rawName: z.string(),
|
||||
quantity: quantity.optional(),
|
||||
unit: z.string().optional(),
|
||||
rawName: z.string().describe(
|
||||
"The ingredient name ONLY — e.g. 'flour', 'egg', 'olive oil'. Never include the " +
|
||||
"quantity or unit here (not '2 cups flour', not '3 eggs'); those go in the separate " +
|
||||
"quantity and unit fields."
|
||||
),
|
||||
quantity: quantity.optional().describe("A number only, e.g. 0.25, 1.5, 2 — never combined with the unit."),
|
||||
unit: z.string().optional().describe("The unit only, e.g. 'cup', 'tbsp', 'g', 'ml' — never combined with the quantity or name."),
|
||||
note: z.string().optional(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { resolveModel, type AiConfig } from "../factory";
|
||||
const ScaledIngredientsSchema = z.object({
|
||||
ingredients: z.array(
|
||||
z.object({
|
||||
rawName: z.string(),
|
||||
quantity: z.string(),
|
||||
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(),
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ const TranslationOutputSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
ingredients: z.array(z.object({
|
||||
rawName: z.string(),
|
||||
rawName: z.string().describe("Translated ingredient name only — never add the quantity or unit, those are handled separately and unaffected by translation."),
|
||||
note: z.string().optional(),
|
||||
})),
|
||||
steps: z.array(z.object({
|
||||
|
||||
Reference in New Issue
Block a user