diff --git a/apps/api/src/routes/recommendations.ts b/apps/api/src/routes/recommendations.ts index afd7824..f219573 100644 --- a/apps/api/src/routes/recommendations.ts +++ b/apps/api/src/routes/recommendations.ts @@ -410,7 +410,9 @@ export async function recommendationRoutes(app: FastifyInstance) { // maten de faktiskt bett om (spec §28) – en kräftskiva eller ett bak tar // tid. Tiden blir mjuk rankning i stället för diskvalificerande filter. // En uttrycklig tidsgräns i förfrågan (q.maxMinutes) respekteras alltid. - const hasExplicitIntent = !!(q.holiday || q.tag || q.search); + // "swap" räknas som uttrycklig avsikt: byt rätt ska inte hårt gömmas av + // hushållets vardags-tidsgräns, annars kvar bara några få snabba rätter. + const hasExplicitIntent = !!(q.holiday || q.tag || q.search || q.view === "swap"); const ctx: RecommendationContext = { mealType: q.mealType, diff --git a/apps/mobile/src/app/swap-meal/[entryId].tsx b/apps/mobile/src/app/swap-meal/[entryId].tsx index 14deb82..f340e99 100644 --- a/apps/mobile/src/app/swap-meal/[entryId].tsx +++ b/apps/mobile/src/app/swap-meal/[entryId].tsx @@ -34,8 +34,11 @@ export default function SwapMealScreen() { const query = useQuery({ queryKey: ["swap-options", mealType], + // view=swap: bredda urvalet – alla lämpliga rätter, inte bara det man har hemma. queryFn: () => - api(`/v1/recommendations/what-to-eat?limit=10&mealType=${mealType}`), + api( + `/v1/recommendations/what-to-eat?limit=20&view=swap&mealType=${mealType}`, + ), }); const swap = useMutation({ diff --git a/packages/recommendation-engine/src/types.ts b/packages/recommendation-engine/src/types.ts index 5b59d4b..4556af9 100644 --- a/packages/recommendation-engine/src/types.ts +++ b/packages/recommendation-engine/src/types.ts @@ -150,9 +150,40 @@ export const NON_PERSONALIZED_WEIGHTS: ScoringWeights = { }; /** Tillåtna vyer för "Vad ska vi äta?". */ -export type RecommendationView = "default" | "taste" | "health" | "pantry"; +export type RecommendationView = "default" | "taste" | "health" | "pantry" | "swap"; -export const RECOMMENDATION_VIEWS: RecommendationView[] = ["default", "taste", "health", "pantry"]; +export const RECOMMENDATION_VIEWS: RecommendationView[] = [ + "default", + "taste", + "health", + "pantry", + "swap", +]; + +/** + * Byt rätt-vyn: användaren vill se ALLA lämpliga rätter att byta till, inte + * bara det de råkar ha hemma. Täckning/utgångsdatum blir bara små tie-breakers; + * betyg, smak och rätt måltidstyp styr. (Löser: "byt rätt" gav bara ~5 val.) + */ +export const SWAP_VIEW_WEIGHTS: ScoringWeights = { + coverage: 6, + expiry: 4, + nutritionFit: 10, + taste: 12, + rating: 14, + season: 5, + holiday: 4, + time: 4, + budget: 4, + variety: 8, + weather: 2, + craving: 10, + mealFit: 14, + dietAffinity: 10, + memoryFit: 6, + tasteFit: 6, + cookingAssumptionFit: 4, +}; /** Fördefinierade vikter för smak-vyn (S4). */ export const TASTE_VIEW_WEIGHTS: ScoringWeights = { @@ -226,6 +257,8 @@ export function viewWeights(view: RecommendationView): ScoringWeights { return HEALTH_VIEW_WEIGHTS; case "pantry": return PANTRY_VIEW_WEIGHTS; + case "swap": + return SWAP_VIEW_WEIGHTS; default: return DEFAULT_WEIGHTS; } diff --git a/packages/validation/src/recommendations.ts b/packages/validation/src/recommendations.ts index 92e23c0..07c6ce0 100644 --- a/packages/validation/src/recommendations.ts +++ b/packages/validation/src/recommendations.ts @@ -22,6 +22,6 @@ export const whatToEatQuerySchema = z.object({ includeLeftovers: z.coerce.boolean().default(true), limit: z.coerce.number().int().min(1).max(20).default(5), /** Personaliseringsvy: smak, hälsa eller lager. Kräver personalization-samtycke. */ - view: z.enum(["default", "taste", "health", "pantry"]).default("default"), + view: z.enum(["default", "taste", "health", "pantry", "swap"]).default("default"), }); export type WhatToEatQuery = z.infer;