const clamp = (value, min, max) => { return Math.min(max, Math.max(min, value)); }; const normalizeText = (value) => { return String(value || '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/\s+/g, ' ') .trim(); }; const GERMAN_COMMON_NAME_HINTS = [ 'weihnachtsstern', 'weinachtsstern', 'einblatt', 'fensterblatt', 'korbmarante', 'glucksfeder', 'gluecksfeder', 'efeutute', 'drachenbaum', 'gummibaum', 'geigenfeige', 'bogenhanf', 'yucca palme', 'gluckskastanie', 'glueckskastanie', ]; // Most of the catalog's German common names are compound nouns ending in one // of these — a hardcoded name list alone (GERMAN_COMMON_NAME_HINTS) only // catches names we've already seen slip through, so this suffix check acts // as a general-purpose backstop for names we haven't hardcoded. const GERMAN_NAME_SUFFIXES = [ 'blume', 'blueten', 'bluten', 'baum', 'strauch', 'kraut', 'wurz', 'wurzel', 'farn', 'palme', 'pflanze', 'hanf', 'tute', 'veilchen', 'dorn', 'kaktus', 'lilie', 'birke', 'weide', 'ranke', 'stern', 'kette', 'schwanz', 'ohren', 'feige', 'moos', 'zwiebel', 'knolle', 'rohr', 'kirsche', ]; const isLikelyGermanCommonName = (value) => { const raw = String(value || '').trim(); if (!raw) return false; if (/[äöüß]/i.test(raw)) return true; const normalized = normalizeText(raw).replace(/[^a-z0-9 ]+/g, ' '); if (!normalized) return false; if (/\b(der|die|das|ein|eine|und|mit|fuer)\b/.test(normalized)) return true; const compact = normalized.replace(/\s+/g, ''); if (GERMAN_NAME_SUFFIXES.some((suffix) => compact.endsWith(suffix))) return true; return GERMAN_COMMON_NAME_HINTS.some((hint) => normalized.includes(hint)); }; // Last-resort safety net applied to the FINAL result regardless of whether // it went through catalog grounding — the AI is instructed to never return a // German name when English was requested, but doesn't always comply, so this // catches the ones that slip through and falls back to the botanical name // (which is language-neutral) rather than showing a mismatched German name // alongside English description/care text. const enforceEnglishName = (result, language) => { if (!result || language !== 'en') return result; if (!isLikelyGermanCommonName(result.name)) return result; if (!result.botanicalName) return result; return { ...result, name: result.botanicalName }; }; const isLikelyBotanicalName = (value, botanicalName) => { const raw = String(value || '').trim(); const botanicalRaw = String(botanicalName || '').trim(); if (!raw) return false; if (normalizeText(raw) === normalizeText(botanicalRaw)) return true; return /^[A-Z][a-z-]+(?:\s[a-z.-]+){1,2}$/.test(raw); }; const findCatalogMatch = (aiResult, entries) => { if (!aiResult || !Array.isArray(entries) || entries.length === 0) return null; const aiBotanical = normalizeText(aiResult.botanicalName); const aiName = normalizeText(aiResult.name); if (!aiBotanical && !aiName) return null; const byExactBotanical = entries.find((entry) => normalizeText(entry.botanicalName) === aiBotanical); if (byExactBotanical) return byExactBotanical; const byExactName = entries.find((entry) => normalizeText(entry.name) === aiName); if (byExactName) return byExactName; if (aiBotanical) { const aiGenus = aiBotanical.split(' ')[0]; if (aiGenus) { const byGenus = entries.find((entry) => normalizeText(entry.botanicalName).startsWith(`${aiGenus} `)); if (byGenus) return byGenus; } } const byContains = entries.find((entry) => { const plantName = normalizeText(entry.name); const botanical = normalizeText(entry.botanicalName); return (aiName && (plantName.includes(aiName) || aiName.includes(plantName))) || (aiBotanical && (botanical.includes(aiBotanical) || aiBotanical.includes(botanical))); }); if (byContains) return byContains; return null; }; const shouldUseCatalogNameOverride = ({ language, aiResult, matchedEntry }) => { const catalogName = String(matchedEntry?.name || '').trim(); if (!catalogName) return false; if (language !== 'en') return true; if (isLikelyBotanicalName(catalogName, matchedEntry?.botanicalName || aiResult?.botanicalName)) { return true; } if (isLikelyGermanCommonName(catalogName)) { return false; } return true; }; const applyCatalogGrounding = (aiResult, catalogEntries, language = 'en') => { const matchedEntry = findCatalogMatch(aiResult, catalogEntries); if (!matchedEntry) { return { grounded: false, result: aiResult }; } const useCatalogName = shouldUseCatalogNameOverride({ language, aiResult, matchedEntry }); return { grounded: true, result: { name: useCatalogName ? matchedEntry.name || aiResult.name : aiResult.name, botanicalName: matchedEntry.botanicalName || aiResult.botanicalName, confidence: clamp(aiResult.confidence || 0.6, 0.05, 0.99), description: aiResult.description || matchedEntry.description || '', careInfo: { waterIntervalDays: Math.max(1, Number(matchedEntry.careInfo?.waterIntervalDays) || Number(aiResult.careInfo?.waterIntervalDays) || 7), light: (matchedEntry.careInfo?.light && matchedEntry.careInfo.light !== 'Unknown') ? matchedEntry.careInfo.light : (aiResult.careInfo?.light || 'Unknown'), temp: (matchedEntry.careInfo?.temp && matchedEntry.careInfo.temp !== 'Unknown') ? matchedEntry.careInfo.temp : (aiResult.careInfo?.temp || 'Unknown'), }, }, }; }; module.exports = { applyCatalogGrounding, enforceEnglishName, findCatalogMatch, isLikelyGermanCommonName, normalizeText, shouldUseCatalogNameOverride, };