Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import type { RecommendationCandidate, RecommendationContext } from "./types.js";
|
||||
|
||||
/**
|
||||
* "Varför rekommenderar vi detta?" (spec §18).
|
||||
* Bygger en ärlig, konkret svensk förklaring ur de deterministiska delpoängen.
|
||||
* Exempel ur specen: "Ni har 92 % av ingredienserna. Kycklingen bör användas
|
||||
* senast i morgon. Rätten ger 58 gram protein per portion …"
|
||||
*/
|
||||
export function buildWhySv(
|
||||
candidate: RecommendationCandidate,
|
||||
ctx: RecommendationContext,
|
||||
parts: Record<string, number>,
|
||||
): string {
|
||||
const sentences: string[] = [];
|
||||
|
||||
const pct = Math.round(candidate.coverage.coverage * 100);
|
||||
if (pct >= 100) {
|
||||
sentences.push("Ni har alla ingredienser hemma.");
|
||||
} else if (pct >= 60) {
|
||||
sentences.push(`Ni har ${pct} % av ingredienserna hemma.`);
|
||||
} else if (pct > 0) {
|
||||
sentences.push(`Ni har ${pct} % av ingredienserna – resten hamnar på inköpslistan.`);
|
||||
}
|
||||
|
||||
const urgent = candidate.coverage.expiringUsed
|
||||
.filter((m) => m.mostUrgentDaysLeft != null)
|
||||
.sort((a, b) => (a.mostUrgentDaysLeft ?? 99) - (b.mostUrgentDaysLeft ?? 99))[0];
|
||||
if (urgent) {
|
||||
const days = urgent.mostUrgentDaysLeft ?? 0;
|
||||
const when = days <= 0 ? "i dag" : days === 1 ? "senast i morgon" : `inom ${days} dagar`;
|
||||
sentences.push(`${capitalize(urgent.displayNameSv)} bör användas ${when}.`);
|
||||
}
|
||||
|
||||
const protein = Math.round(candidate.nutritionPerPortion.proteinG);
|
||||
if ((parts.nutritionFit ?? 0) >= 0.7 && protein >= 25) {
|
||||
sentences.push(`Rätten ger ${protein} gram protein per portion.`);
|
||||
}
|
||||
if (ctx.isTrainingDay && protein >= 35) {
|
||||
sentences.push("Bra val på en träningsdag.");
|
||||
}
|
||||
|
||||
if ((parts.rating ?? 0) >= 0.8 && candidate.householdRating != null) {
|
||||
sentences.push("Liknande rätter har fått höga betyg av hushållet.");
|
||||
}
|
||||
|
||||
if ((parts.holiday ?? 0) >= 1 && ctx.activeHolidayTags.length > 0) {
|
||||
sentences.push("Passar den kommande högtiden.");
|
||||
} else if ((parts.season ?? 0) >= 1) {
|
||||
sentences.push("Råvarorna är i säsong just nu.");
|
||||
}
|
||||
|
||||
if ((parts.time ?? 0) >= 1 && ctx.isWeekday) {
|
||||
sentences.push(`Klar på ${candidate.totalTimeMinutes} minuter.`);
|
||||
}
|
||||
|
||||
if ((parts.budget ?? 0) >= 1 && candidate.estimatedCostMinorPerPortion != null) {
|
||||
sentences.push(
|
||||
`Cirka ${Math.round(candidate.estimatedCostMinorPerPortion / 100)} kr per portion.`,
|
||||
);
|
||||
}
|
||||
|
||||
if ((parts.craving ?? 0) >= 1) {
|
||||
sentences.push("Matchar det du är sugen på.");
|
||||
}
|
||||
|
||||
if (sentences.length === 0) {
|
||||
sentences.push("En balanserad rätt som passar er profil.");
|
||||
}
|
||||
|
||||
return sentences.join(" ");
|
||||
}
|
||||
|
||||
function capitalize(s: string): string {
|
||||
return s.length === 0 ? s : s[0]!.toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
/** Kompakt kontextsammanfattning (utan persondata) till AAMOS RANK_RECIPES. */
|
||||
export function summarizeContext(ctx: RecommendationContext): string {
|
||||
const parts = [
|
||||
`måltid=${ctx.mealType}`,
|
||||
`personer=${ctx.persons}`,
|
||||
`säsong=${ctx.currentSeason}`,
|
||||
ctx.isWeekday ? "vardag" : "helg",
|
||||
];
|
||||
if (ctx.maxMinutes != null) parts.push(`maxMinuter=${ctx.maxMinutes}`);
|
||||
if (ctx.activeHolidayTags.length > 0) parts.push(`högtider=${ctx.activeHolidayTags.join("+")}`);
|
||||
if (ctx.cravingTags && ctx.cravingTags.length > 0)
|
||||
parts.push(`sugen_på=${ctx.cravingTags.join("+")}`);
|
||||
if (ctx.cravingCuisine) parts.push(`kök=${ctx.cravingCuisine}`);
|
||||
return parts.join(", ");
|
||||
}
|
||||
Reference in New Issue
Block a user