Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
@@ -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(", ");
}