Files
Cibello-app/packages/recommendation-engine/src/craving.ts
T
2026-08-14 20:28:56 +07:00

101 lines
2.9 KiB
TypeScript

import type { Cuisine } from "@app/shared-types";
export interface ParsedCraving {
tags: string[];
cuisine?: Cuisine | undefined;
maxKcal?: number | undefined;
keywords: string[];
}
/**
* Deterministisk grundtolkning av "Jag är sugen på …" (spec §19).
* Täcker vanliga svenska uttryck utan AI-anrop; AAMOS kan förfina tolkningen
* (GENERATE_RECIPE_OPTIONS) men denna bas fungerar alltid, även offline-nära.
*/
const CUISINE_KEYWORDS: Array<[Cuisine, string[]]> = [
["thai", ["thai", "thailändskt", "asiatiskt"]],
["japanese", ["japanskt", "sushi", "ramen"]],
["chinese", ["kinesiskt", "wok"]],
["korean", ["koreanskt", "kimchi"]],
["vietnamese", ["vietnamesiskt", "pho"]],
["indian", ["indiskt", "curry"]],
["italian", ["italienskt", "pasta", "pizza"]],
["french", ["franskt"]],
["spanish", ["spanskt", "tapas", "paella"]],
["greek", ["grekiskt", "gyros", "tzatziki"]],
["mexican", ["mexikanskt", "tacos", "tex mex", "texmex"]],
["american", ["amerikanskt", "burgare", "hamburgare"]],
["turkish", ["turkiskt", "kebab"]],
["lebanese", ["libanesiskt", "meze"]],
["moroccan", ["marockanskt", "tagine"]],
["swedish", ["svenskt", "husman", "husmanskost"]],
["nordic", ["nordiskt"]],
];
const TAG_KEYWORDS: Array<[string, string[]]> = [
["creamy", ["krämigt", "krämig", "gräddigt"]],
["spicy", ["starkt", "stark", "hett", "chili"]],
["fresh", ["fräscht", "fräsch", "lätt", "somrigt"]],
["comfort", ["comfort", "mysmat", "husman", "tröstmat"]],
["luxury", ["lyxigt", "lyx", "fest"]],
["quick", ["snabbt", "snabb", "enkelt", "enkel"]],
["kid_friendly", ["barnvänligt", "barnvänlig", "barn"]],
["high_protein", ["proteinrikt", "protein"]],
["vegetarian", ["vegetariskt", "vegetarisk", "vego"]],
["vegan", ["veganskt", "vegansk"]],
["soup", ["soppa"]],
["stew", ["gryta", "långkok"]],
["grill", ["grill", "grillat"]],
];
export function parseCraving(text: string): ParsedCraving {
const lower = text.toLowerCase();
const tags: string[] = [];
let cuisine: Cuisine | undefined;
for (const [c, words] of CUISINE_KEYWORDS) {
if (words.some((w) => lower.includes(w))) {
cuisine = c;
break;
}
}
for (const [tag, words] of TAG_KEYWORDS) {
if (words.some((w) => lower.includes(w))) tags.push(tag);
}
let maxKcal: number | undefined;
const kcalMatch = lower.match(/under\s+(\d{2,4})\s*(?:kcal|kalorier)/);
if (kcalMatch?.[1]) maxKcal = Number(kcalMatch[1]);
const STOPWORDS = new Set([
"jag",
"är",
"sugen",
"på",
"vill",
"ha",
"något",
"lite",
"och",
"med",
"gärna",
"känner",
"för",
"mig",
"idag",
"ikväll",
"kväll",
"mat",
"äta",
"laga",
"göra",
"en",
"ett",
]);
const keywords = lower
.split(/[^a-zåäö0-9]+/)
.filter((w) => w.length >= 3 && !STOPWORDS.has(w));
return { tags, cuisine, maxKcal, keywords };
}