diff --git a/src/App.jsx b/src/App.jsx index e81aaaf3a..f1c784b5d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -51,6 +51,7 @@ const GalaBoard = React.lazy(() => import("./pages/GalaPage")); const IngredientPage = React.lazy(() => import("./pages/ingredients")); const Brandinator = React.lazy(() => import("./pages/Brandinator")); const BugPage = React.lazy(() => import("./pages/bug")); +const SpellCheck = React.lazy(() => import("./pages/spell-check")); const TaxonomyWalk = React.lazy(() => import("./pages/taxonomyWalk")); @@ -348,6 +349,8 @@ export default function App() { ) } /> + } /> + {showFlaggedImage && ( } /> )} diff --git a/src/pages/spell-check/HighlightIndexes.tsx b/src/pages/spell-check/HighlightIndexes.tsx new file mode 100644 index 000000000..b31237b6b --- /dev/null +++ b/src/pages/spell-check/HighlightIndexes.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +export default function Highlight({ text, highlight }) { + let rep = ""; + // console.log(highlight); + let lastIndex = 0; + highlight.forEach(({ color, index }) => { + rep = + rep + + text.slice(lastIndex, index) + + `${text[index]}`; + lastIndex = index + 1; + }); + + rep = rep + text.slice(lastIndex); + + return

; +} diff --git a/src/pages/spell-check/ShowImage.tsx b/src/pages/spell-check/ShowImage.tsx new file mode 100644 index 000000000..8681e086c --- /dev/null +++ b/src/pages/spell-check/ShowImage.tsx @@ -0,0 +1,105 @@ +import * as React from "react"; +import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; +import off from "../../off"; +import { OFF_IMAGE_URL } from "../../const"; +import { Button, Stack } from "@mui/material"; +import { useCountry } from "../../contexts/CountryProvider"; +interface ShowImageProps { + barcode: string; + images?: Record; +} + +export default function ShowImage(props: ShowImageProps) { + const { barcode, images } = props; + const [country] = useCountry(); + + const [selectedImageId, setSelectedImageId] = React.useState(null); + const availableIngredientImages = React.useMemo< + { country: string; imgid: number }[] + >( + () => + Object.keys(images ?? {}) + .map((key) => { + if (key.startsWith("ingredients_")) { + return { country: key.split("_")[1], imgid: images[key].imgid }; + } + return null; + }) + .filter(Boolean), + [images], + ); + + React.useEffect(() => { + if (images?.[`ingredients_${country}`] !== undefined) { + setSelectedImageId(images?.[`ingredients_${country}`]?.imgid ?? null); + } else { + setSelectedImageId(availableIngredientImages[0]?.imgid ?? null); + } + }, [images, availableIngredientImages]); + + const availableImgids = React.useMemo(() => { + const ids = Object.values(images ?? {}) + .map((img) => img.imgid) + .filter((id) => id !== undefined); + + // Dumb filter + return ids.filter((id, index) => !ids.slice(0, index).includes(id)); + }, [images]); + + const imageUrl = selectedImageId + ? `${OFF_IMAGE_URL}/${off.getFormatedBarcode( + barcode, + )}/${selectedImageId}.jpg` + : ""; + + return ( +

+ + {availableIngredientImages.map(({ country, imgid }) => ( + + ))} + + + +
+ +
+
+
+ + {availableImgids.map((id) => ( + + ))} + +
+ ); +} diff --git a/src/pages/spell-check/TextCorrection/DiffPrint.tsx b/src/pages/spell-check/TextCorrection/DiffPrint.tsx new file mode 100644 index 000000000..bb227e108 --- /dev/null +++ b/src/pages/spell-check/TextCorrection/DiffPrint.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { Typography } from "@mui/material"; + +interface DiffPrintProps { + text: string; + mask: string; + /** + * Map mask character to style + */ + mapping: Record; +} +export function DiffPrint(props: DiffPrintProps) { + const { text, mask, mapping } = props; + + return ( + + {text.split("").map((char, index) => ( + + {char} + + ))} + + ); +} diff --git a/src/pages/spell-check/TextCorrection/IngeredientDisplay.tsx b/src/pages/spell-check/TextCorrection/IngeredientDisplay.tsx new file mode 100644 index 000000000..5ef6f8f44 --- /dev/null +++ b/src/pages/spell-check/TextCorrection/IngeredientDisplay.tsx @@ -0,0 +1,239 @@ +import * as React from "react"; +import Tooltip from "@mui/material/Tooltip"; + +import { useTheme } from "@mui/material"; + +import off from "../../../off"; + +type BooleanEstimation = "no" | "yes" | "maybe"; +type ParsedIngredientsType = { + ciqual_proxy_food_code?: string; + id: string; + ingredients?: ParsedIngredientsType[]; + is_in_taxonomy: 0 | 1; + origins?: string; + percent_estimate: number; + percent_max: number; + percent_min: number; + text: string; + vegan: BooleanEstimation; + vegetarian: BooleanEstimation; +}; + +function getColor(ingredient: ParsedIngredientsType) { + if (ingredient.is_in_taxonomy === 1) return "green"; + if (ingredient.is_in_taxonomy === 0) return "orange"; + if (ingredient.ciqual_proxy_food_code !== undefined) return "green"; + if (ingredient.vegetarian !== undefined) return "lightgreen"; + if (ingredient.ingredients !== undefined) return "blue"; + return "orange"; +} + +function getTitle(ingredient: ParsedIngredientsType) { + if (ingredient.ciqual_proxy_food_code !== undefined) + return "This ingredient has CIQUAL id"; + if (ingredient.vegetarian !== undefined) return "recognised as a vegetarian"; + if (ingredient.ingredients !== undefined) return "contains sub ingredients"; + return `unknown ingredient: ${ingredient.text}"`; +} +function ColorText({ + text, + ingredients, +}: { + text: string; + ingredients?: ParsedIngredientsType[]; +}) { + const theme = useTheme(); + const isDark = theme.palette.mode === "dark"; + + const lightColors = ["gray", "black"]; + const darkColors = ["lightgray", "white"]; + + if (ingredients === undefined) { + // Without parsing, we just split with coma + return text.split(",").map((txt, i) => ( + + + {txt} + + {i === text.split(",").length - 1 ? "" : ","} + + )); + } + + const flattendIngredients = ingredients.flatMap( + ({ ingredients, ...ingredient }) => [ingredient, ...(ingredients || [])], + ); + + let lastIndex = 0; + + return [ + ...flattendIngredients.map((ingredient, i) => { + // Don't ask me why OFF use this specific character + const ingredientText = ingredient.text.replace("‚", ",").toLowerCase(); + + const startIndex = text.toLowerCase().indexOf(ingredientText, lastIndex); + if (startIndex < 0) { + return null; + } + const endIndex = startIndex + ingredient.text.length; + + const prefix = text.slice(lastIndex, startIndex); + const ingredientName = text.slice(startIndex, endIndex); + lastIndex = endIndex; + + return ( + + {prefix} + + + + {ingredientName} + + + + ); + }), + text.slice(lastIndex, text.length), + ]; +} + +export function useIngredientParsing() { + const [isLoading, setLoading] = React.useState(false); + const [parsings, setParsing] = React.useState({}); + + async function fetchIngredients(text: string, lang: string) { + if (parsings[text] !== undefined) { + return; + } + + setLoading(true); + const parsing = await off.getIngedrientParsing({ + text, + lang, + }); + const ingredients = parsing.data?.product?.ingredients; + setParsing((prev) => ({ ...prev, [text]: ingredients })); + setLoading(false); + } + + return { isLoading, fetchIngredients, parsings }; +} + +export function IngeredientDisplay(props) { + const { text, onChange, parsings } = props; + + const theme = useTheme(); + const isDark = theme.palette.mode === "dark"; + + return ( +
+
+ +