338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
import type { AamosTaskType } from "./tasks.js";
|
||
|
||
/**
|
||
* Deterministiska mock-svar per uppgiftstyp. Realistiska nog för att bygga
|
||
* UI-flöden och tester mot – inklusive osäkerhetsfall (requiresConfirmation,
|
||
* lågt confidence) så att korrigerings-UX:et alltid övas (spec §61.4–5).
|
||
*/
|
||
export function mockOutputFor(taskType: AamosTaskType, input: unknown): unknown {
|
||
switch (taskType) {
|
||
case "ANALYZE_FRIDGE_IMAGE":
|
||
case "ANALYZE_PANTRY_IMAGE":
|
||
return {
|
||
items: [
|
||
{
|
||
detectedName: "mjölk",
|
||
canonicalIngredientId: "milk_3",
|
||
brand: "Arla",
|
||
estimatedQuantity: 0.7,
|
||
unit: "LITER",
|
||
bestBeforeDate: null,
|
||
confidence: 0.84,
|
||
requiresConfirmation: true,
|
||
boundingBox: null,
|
||
},
|
||
{
|
||
detectedName: "crème fraîche",
|
||
canonicalIngredientId: "creme_fraiche",
|
||
brand: null,
|
||
estimatedQuantity: 1,
|
||
unit: "COUNT",
|
||
bestBeforeDate: null,
|
||
confidence: 0.61,
|
||
requiresConfirmation: true,
|
||
boundingBox: null,
|
||
},
|
||
{
|
||
detectedName: "okänd burk",
|
||
canonicalIngredientId: null,
|
||
brand: null,
|
||
estimatedQuantity: null,
|
||
unit: null,
|
||
bestBeforeDate: null,
|
||
confidence: 0.2,
|
||
requiresConfirmation: true,
|
||
boundingBox: null,
|
||
},
|
||
],
|
||
imageQualityIssues: [],
|
||
};
|
||
|
||
case "ANALYZE_MEAL_IMAGE": {
|
||
const hasContext =
|
||
typeof input === "object" &&
|
||
input !== null &&
|
||
(input as { recipeContext?: unknown }).recipeContext != null;
|
||
return {
|
||
matchesRecipeContext: hasContext ? true : null,
|
||
portionFractionEstimate: 1,
|
||
kcalRange: { min: 650, max: 750, mostLikely: 700 },
|
||
components: [
|
||
{
|
||
name: "kycklingfilé",
|
||
canonicalIngredientId: "chicken_breast",
|
||
estimatedGrams: 150,
|
||
confidence: 0.8,
|
||
},
|
||
{
|
||
name: "ris",
|
||
canonicalIngredientId: "rice_white",
|
||
estimatedGrams: 180,
|
||
confidence: 0.75,
|
||
},
|
||
],
|
||
confidence: 0.72,
|
||
};
|
||
}
|
||
|
||
case "READ_RECEIPT":
|
||
return {
|
||
storeName: "ICA Supermarket",
|
||
purchaseDate: "2026-08-01",
|
||
lines: [
|
||
{
|
||
rawText: "KYCKL FILE 925G",
|
||
normalizedName: "Kycklingfilé 925 g",
|
||
canonicalIngredientId: "chicken_breast",
|
||
quantity: 925,
|
||
unit: "GRAM",
|
||
unitPriceMinor: 11990,
|
||
totalPriceMinor: 11990,
|
||
isDiscount: false,
|
||
confidence: 0.9,
|
||
},
|
||
{
|
||
rawText: "MELLANMJ 1.5L",
|
||
normalizedName: "Mellanmjölk 1,5 l",
|
||
canonicalIngredientId: "milk_1_5",
|
||
quantity: 1.5,
|
||
unit: "LITER",
|
||
unitPriceMinor: 1890,
|
||
totalPriceMinor: 1890,
|
||
isDiscount: false,
|
||
confidence: 0.87,
|
||
},
|
||
],
|
||
totalMinor: 13880,
|
||
discountTotalMinor: 0,
|
||
confidence: 0.85,
|
||
};
|
||
|
||
case "READ_NUTRITION_LABEL":
|
||
return {
|
||
basis: "per_100_g",
|
||
values: {
|
||
kcal: 106,
|
||
proteinG: 22,
|
||
carbsG: 0,
|
||
fatG: 2,
|
||
saturatedFatG: 0.6,
|
||
fiberG: 0,
|
||
sugarG: 0,
|
||
saltG: 0.2,
|
||
},
|
||
ingredientsText: "Kycklingfilé (100 %)",
|
||
allergensDeclared: [],
|
||
gtin: null,
|
||
productName: "Kycklingfilé",
|
||
brand: null,
|
||
confidence: 0.88,
|
||
};
|
||
|
||
case "READ_EXPIRY_DATE":
|
||
return { date: "2026-08-06", dateKind: "best_before", confidence: 0.9 };
|
||
|
||
case "NORMALIZE_PRODUCTS": {
|
||
const rawNames =
|
||
typeof input === "object" && input !== null
|
||
? ((input as { rawNames?: string[] }).rawNames ?? [])
|
||
: [];
|
||
return {
|
||
matches: rawNames.map((raw) => ({
|
||
raw,
|
||
canonicalIngredientId: null,
|
||
normalizedName: raw.toLowerCase(),
|
||
confidence: 0.4,
|
||
})),
|
||
};
|
||
}
|
||
|
||
case "DEDUPLICATE_INVENTORY":
|
||
return { duplicateGroups: [] };
|
||
|
||
case "STRUCTURE_RECIPE_TEXT":
|
||
return {
|
||
titleSv: "Snabb kycklingpasta",
|
||
descriptionSv: "Krämig vardagspasta med kyckling och spenat.",
|
||
ingredients: [
|
||
{
|
||
rawText: "400 g kycklingfilé",
|
||
canonicalIngredientId: "chicken_breast",
|
||
displayNameSv: "Kycklingfilé",
|
||
quantity: 400,
|
||
unit: "GRAM",
|
||
optional: false,
|
||
confidence: 0.9,
|
||
},
|
||
{
|
||
rawText: "300 g pasta",
|
||
canonicalIngredientId: "pasta_dry",
|
||
displayNameSv: "Pasta",
|
||
quantity: 300,
|
||
unit: "GRAM",
|
||
optional: false,
|
||
confidence: 0.9,
|
||
},
|
||
],
|
||
steps: [
|
||
{ instructionSv: "Koka pastan enligt anvisning.", timerSeconds: 600, temperatureC: null },
|
||
{
|
||
instructionSv: "Stek kycklingen tills genomstekt.",
|
||
timerSeconds: null,
|
||
temperatureC: null,
|
||
},
|
||
],
|
||
prepTimeMinutes: 10,
|
||
cookTimeMinutes: 15,
|
||
portions: 4,
|
||
suggestedCuisine: "italian",
|
||
suggestedMealTypes: ["dinner"],
|
||
confidence: 0.82,
|
||
};
|
||
|
||
case "GENERATE_RECIPE_OPTIONS":
|
||
return {
|
||
suggestions: [
|
||
{
|
||
titleSv: "Krämig kycklingwok med grönsaker",
|
||
descriptionSv: "Snabb wok på det som finns hemma.",
|
||
ingredients: [
|
||
{
|
||
canonicalIngredientId: "chicken_breast",
|
||
displayNameSv: "Kycklingfilé",
|
||
quantity: 400,
|
||
unit: "GRAM",
|
||
},
|
||
{
|
||
canonicalIngredientId: "rice_white",
|
||
displayNameSv: "Ris",
|
||
quantity: 3,
|
||
unit: "DECILITER",
|
||
},
|
||
],
|
||
steps: ["Koka riset.", "Woka kycklingen.", "Blanda och servera."],
|
||
estimatedTimeMinutes: 25,
|
||
confidence: 0.7,
|
||
},
|
||
],
|
||
};
|
||
|
||
case "GENERATE_RECIPE_CANDIDATES": {
|
||
// Deterministisk mock: returnerar en enkel kandidat per target-cell
|
||
const inp = input as {
|
||
targetMatrix?: Array<{ mealType: string; mainIngredientId: string; dietVariant: string; count: number }>;
|
||
canonicalIngredientsCatalog?: Array<{ id: string; nameSv: string; category: string; defaultUnit: string }>;
|
||
};
|
||
const targets = inp.targetMatrix ?? [];
|
||
const catalog = inp.canonicalIngredientsCatalog ?? [];
|
||
const candidates = [];
|
||
for (const t of targets) {
|
||
const mainIng = catalog.find((c) => c.id === t.mainIngredientId);
|
||
for (let i = 0; i < Math.min(t.count, 2); i++) {
|
||
candidates.push({
|
||
titleSv: `Mock-${mainIng?.nameSv ?? t.mainIngredientId} ${t.dietVariant} ${i + 1}`,
|
||
descriptionSv: `Ett enkelt vardagsrecept med ${mainIng?.nameSv ?? t.mainIngredientId}.`,
|
||
cuisine: "swedish",
|
||
mealTypes: [t.mealType],
|
||
prepTimeMinutes: 10,
|
||
cookTimeMinutes: 20,
|
||
portions: 4,
|
||
spiceLevel: 1,
|
||
ingredients: [
|
||
{
|
||
canonicalIngredientId: t.mainIngredientId,
|
||
displayNameSv: mainIng?.nameSv ?? t.mainIngredientId,
|
||
quantity: 500,
|
||
unit: mainIng?.defaultUnit ?? "GRAM",
|
||
optional: false,
|
||
note: null,
|
||
},
|
||
{
|
||
canonicalIngredientId: "onion",
|
||
displayNameSv: "gul lök",
|
||
quantity: 2,
|
||
unit: "COUNT",
|
||
optional: false,
|
||
note: null,
|
||
},
|
||
{
|
||
canonicalIngredientId: "rice_white",
|
||
displayNameSv: "ris",
|
||
quantity: 3,
|
||
unit: "DECILITER",
|
||
optional: false,
|
||
note: null,
|
||
},
|
||
],
|
||
steps: [
|
||
{ instructionSv: "Förbered ingredienserna.", timerSeconds: null, temperatureC: null, tip: null },
|
||
{ instructionSv: "Stek och låt koka klart.", timerSeconds: 900, temperatureC: null, tip: null },
|
||
],
|
||
storageGuidanceSv: "Förvara i kylskåp upp till 3 dagar.",
|
||
mealPrepFriendly: false,
|
||
freezerFriendly: false,
|
||
confidence: 0.85,
|
||
});
|
||
}
|
||
}
|
||
return { candidates, rejectedPrompts: [] };
|
||
}
|
||
|
||
case "RANK_RECIPES": {
|
||
const ids =
|
||
typeof input === "object" && input !== null
|
||
? ((input as { candidateIds?: string[] }).candidateIds ?? [])
|
||
: [];
|
||
return { rankedIds: ids, rationaleSv: null };
|
||
}
|
||
|
||
case "PARSE_CRAVING":
|
||
return { tags: ["creamy"], cuisine: null, maxKcal: null, confidence: 0.6 };
|
||
|
||
case "UPDATE_USER_MEMORY":
|
||
return { memoryUpdates: [] };
|
||
|
||
case "GENERATE_WEEK_PLAN":
|
||
return { entries: [], confidence: 0.5 };
|
||
|
||
case "MODERATE_RECIPE":
|
||
return { flags: [], recommendation: "approve", confidence: 0.9 };
|
||
|
||
case "EXPORT_TRAINING_SAMPLE": {
|
||
const samples =
|
||
typeof input === "object" && input !== null
|
||
? ((input as { samples?: unknown[] }).samples ?? [])
|
||
: [];
|
||
return {
|
||
batchId: `mock-batch-${Date.now()}`,
|
||
accepted: samples.length,
|
||
rejected: 0,
|
||
};
|
||
}
|
||
|
||
case "TRANSLATE_RECIPE": {
|
||
// Deterministisk pseudo-översättning: bevarar alla tal och stegstruktur,
|
||
// markerar texten med målspråket så flödet är testbart utan riktig AI.
|
||
const inp = input as {
|
||
targetLanguageTag: string;
|
||
title: string;
|
||
description: string | null;
|
||
storageGuidance: string | null;
|
||
steps: { stepNumber: number; instruction: string; tip: string | null }[];
|
||
};
|
||
const tag = `[${inp.targetLanguageTag}]`;
|
||
return {
|
||
title: `${tag} ${inp.title}`,
|
||
description: inp.description ? `${tag} ${inp.description}` : null,
|
||
storageGuidance: inp.storageGuidance ? `${tag} ${inp.storageGuidance}` : null,
|
||
steps: inp.steps.map((s) => ({
|
||
stepNumber: s.stepNumber,
|
||
instruction: `${tag} ${s.instruction}`,
|
||
tip: s.tip ? `${tag} ${s.tip}` : null,
|
||
})),
|
||
confidence: 0.85,
|
||
};
|
||
}
|
||
}
|
||
}
|