From d0900ad7fd25e67e7a591dbc666aa75e06e5e901 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 20 Aug 2026 14:46:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(recipes):=20surligne=20les=20tech=20steps?= =?UTF-8?q?=20d=C3=A9tect=C3=A9s=20dans=20les=20=C3=A9tapes,=20avec=20tool?= =?UTF-8?q?tip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose côté UI ce que tech-step-matcher.ts détecte déjà à la sauvegarde (Step.techSteps) mais qui restait backend-only : dans le panneau détail d'une recette, les mots exacts ayant déclenché une technique sont surlignés, avec un tooltip (survol/focus clavier) donnant son nom. - tech-step-matcher.ts : matchTechStepSpans(description, mappings) expose désormais {techStepId, start, end} en plus de la simple séquence d'ids (déjà calculé en interne, jusqu'ici jeté). matchTechSteps devient un wrapper fin dessus — aucun changement à ses ~12 tests existants ni à recipe-translation.ts. - StepTechStep gagne start/end (nullable, pas de backfill — même leçon que l'incident de migration ingredient_unit_catalog : NOT NULL sans défaut sur une table déjà peuplée casse le déploiement). Une ligne pré-existante sans span est simplement omise de la réponse API plutôt que de fuiter un null, jusqu'à ce que la recette soit resauvegardée. - recipe.service.ts : createRecipe/updateRecipe persistent start/end ; StepView expose techSteps: { techStep: {id,key}, start, end }[]. Le recalcul complet à chaque édition (ajout/modif/suppression d'étape) était déjà garanti par le delete-then-recreate existant d'updateRecipe — testé explicitement (nouveau test "recomputes techniques from scratch..."). - Frontend : StepDescription.tsx (découpe le texte via highlight-tech-steps.ts, pur et testé) remplace le

brut dans RecipeDetailPanel. Nouveau Tooltip.tsx (composants/ui, CSS pur, aucune lib externe — même esprit que Dialog.tsx) : un + + ); + })} +

+ ); +} diff --git a/apps/web/src/features/recipes/highlight-tech-steps.ts b/apps/web/src/features/recipes/highlight-tech-steps.ts new file mode 100644 index 0000000..735cd5a --- /dev/null +++ b/apps/web/src/features/recipes/highlight-tech-steps.ts @@ -0,0 +1,47 @@ +import type { StepTechStepView } from "@batch-cooking/shared"; + +/** + * One run of a step's `description` — either plain text, or the exact + * words that triggered a technique match (`techStep` set). What + * `StepDescription.tsx` renders: plain segments as-is, technique segments + * wrapped in a highlighted, tooltip-bearing ``. + */ +export interface DescriptionSegment { + text: string; + techStep: StepTechStepView["techStep"] | null; +} + +/** + * Splits `description` into an ordered sequence of plain/technique + * {@link DescriptionSegment}s using each `techSteps` entry's `start`/`end` + * (see `StepTechStepView`, resolved server-side by + * `tech-step-matcher.ts`'s `matchTechStepSpans`). + * + * `techSteps` is expected already sorted by `start` (the API returns it in + * `StepTechStep.order`, which *is* reading order — see that model's schema + * doc comment) but this re-sorts defensively rather than assuming it, and + * silently drops any entry whose bounds don't make sense against + * `description` (`start < 0`, `end > description.length`, `start >= end`, + * or overlapping a previously-accepted entry) — a malformed/out-of-date + * span degrades to "just don't highlight that one" rather than a garbled + * slice or a crash. + */ +export function splitDescriptionByTechSteps( + description: string, + techSteps: StepTechStepView[], +): DescriptionSegment[] { + const sorted = [...techSteps].sort((a, b) => a.start - b.start); + + const segments: DescriptionSegment[] = []; + let cursor = 0; + for (const { techStep, start, end } of sorted) { + if (start < 0 || end > description.length || start >= end || start < cursor) continue; + if (start > cursor) segments.push({ text: description.slice(cursor, start), techStep: null }); + segments.push({ text: description.slice(start, end), techStep }); + cursor = end; + } + if (cursor < description.length) { + segments.push({ text: description.slice(cursor), techStep: null }); + } + return segments; +} diff --git a/apps/web/src/features/recipes/recipes.scss b/apps/web/src/features/recipes/recipes.scss index a1dd7b3..4523b00 100644 --- a/apps/web/src/features/recipes/recipes.scss +++ b/apps/web/src/features/recipes/recipes.scss @@ -537,6 +537,34 @@ } } +// A step's detected-technique keyword (see StepDescription.tsx) — a real +//