fix(byt-ratt): bredda urvalet - alla lampliga ratter, inte bara det man har hemma

Ny 'swap'-vy i rekommendationsmotorn med lag tackningsvikt (30->6) sa
betyg/smak/ratt maltidstyp styr i stallet for 'har hemma'. Byt rätt-
skarmen anropar view=swap, kringgar vardags-tidsgransen och hojer limit
till 20. Loser: bara ~5 val vid byte.
This commit is contained in:
Claude
2026-08-19 23:33:32 +00:00
parent e3e3fdfd73
commit 5867a95d64
4 changed files with 43 additions and 5 deletions
+3 -1
View File
@@ -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,
+4 -1
View File
@@ -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<WhatToEatResponse>(`/v1/recommendations/what-to-eat?limit=10&mealType=${mealType}`),
api<WhatToEatResponse>(
`/v1/recommendations/what-to-eat?limit=20&view=swap&mealType=${mealType}`,
),
});
const swap = useMutation({
+35 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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<typeof whatToEatQuerySchema>;