import { useState, useEffect, useRef, useCallback } from "react"; import { Mic, MicOff } from "lucide-react"; import { Messages } from "../../../Components/Notifications/Messages"; import { Tooltip } from "antd"; const EnhancedVoiceForm = ({ setVoiceData, uomNames }) => { const [messageType, setMessageType] = useState(null); const [messageData, setMessageData] = useState(null); const [formData, setFormData] = useState({ product: "", qty: "", uom: "", price: "" }); const [transcript, setTranscript] = useState(""); const [isListening, setIsListening] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState(""); const [confidence, setConfidence] = useState(0); const recognitionRef = useRef(null); // Improved extractEntities function with better pattern recognition // Improved extractEntities function with better pattern recognition const extractEntities = (text) => { if (!text?.trim()) return { product: "", qty: "", uom: "", price: "" }; const cleanText = text.toLowerCase().trim(); // UOM mappings (comprehensive list with all common units) const uomMappings = { // Existing units nos: ["nos", "no", "number", "numbers", "piece", "pieces", "pcs", "pc", "unit", "units", "each"], "m.w": ["m.w", "mw", "meter weight", "meter weight", "m weight", "m w"], "l.w": ["l.w", "lw", "length width", "length and width", "l width", "l w", "length", "width"], "h.w": ["h.w", "hw", "height width", "height and width", "h width", "h w", "height", "width"], box: ["box", "boxes", "bx", "carton", "cartons", "case", "cases"], plt: ["plt", "plate", "plates", "plat", "plats"], pcs: ["pcs", "pc", "piece", "pieces", "unit", "units", "each"], // Weight/Mass units kgs: ["kgs", "kilos", "kilograms", "kilogrammes"], kg: ["kg", "kilo", "kilogram", "kilogramme"], gram: ["gram", "grams", "g", "gms", "gramme", "grammes"], ton: ["ton", "tons", "tonne", "tonnes", "t"], pound: ["lb", "lbs", "pound", "pounds"], ounce: ["oz", "ounce", "ounces"], // Volume units ltr: ["ltr", "l", "liter", "liters", "litre", "litres", "liquid", "fluid"], ml: ["ml", "mili", "milli", "millilitre", "millilitres", "milliliter", "milliliters", "mili litre", "mili liter", "milli litre", "milli liter"], gallon: ["gal", "gallon", "gallons"], pint: ["pt", "pint", "pints"], cup: ["cup", "cups"], tablespoon: ["tbsp", "tablespoon", "tablespoons", "table spoon", "table spoons"], teaspoon: ["tsp", "teaspoon", "teaspoons", "tea spoon", "tea spoons"], // Length/Distance units meter: ["meter", "meters", "m", "mtr", "metre", "metres", "length", "distance"], centimeter: ["cm", "centimeter", "centimeters", "centimetre", "centimetres", "centi meter", "centi meters"], millimeter: ["mm", "millimeter", "millimeters", "millimetre", "millimetres", "milli meter", "milli meters"], kilometer: ["km", "kilometer", "kilometers", "kilometre", "kilometres", "kilo meter", "kilo meters"], inch: ["in", "inch", "inches"], foot: ["ft", "foot", "feet"], yard: ["yd", "yard", "yards"], // Area units sqm: ["sq m", "sqm", "square meter", "square meters", "square metre", "square metres"], sqft: ["sq ft", "sqft", "square foot", "square feet"], acre: ["acre", "acres"], // Time units second: ["sec", "second", "seconds", "s"], minute: ["min", "minute", "minutes"], hour: ["hr", "hour", "hours", "h"], day: ["day", "days"], week: ["week", "weeks"], month: ["month", "months"], year: ["year", "years", "yr", "yrs"], // Temperature units celsius: ["°C", "celsius", "centigrade", "c", "degree celsius", "degrees celsius"], fahrenheit: ["°F", "fahrenheit", "f", "degree fahrenheit", "degrees fahrenheit"], kelvin: ["K", "kelvin", "degree kelvin", "degrees kelvin"], // Electrical units watt: ["watt", "watts", "w", "power", "wt"], volt: ["volt", "volts", "v", "voltage", "vt"], ampere: ["amp", "amps", "ampere", "amperes", "a"], ohm: ["ohm", "ohms", "Ω"], // Other common units dozen: ["dozen", "dozens", "dz"], pair: ["pair", "pairs", "pr"], set: ["set", "sets"], pack: ["pack", "packs", "packet", "packets", 'pac'], roll: ["roll", "rolls"], sheet: ["sheet", "sheets"], bottle: ["bottle", "bottles", "btl"], can: ["can", "cans"], jar: ["jar", "jars"], tube: ["tube", "tubes"] }; // Number words mapping (compound phrases first to match them before individual words) const numberWords = { "three quarters": "0.75", "two quarters": "0.5", "one quarter": "0.25", "a quarter": "0.25", "half": "0.5", "one half": "0.5", "a half": "0.5", zero: "0", one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10", eleven: "11", twelve: "12", thirteen: "13", fourteen: "14", fifteen: "15", sixteen: "16", seventeen: "17", eighteen: "18", nineteen: "19", twenty: "20", thirty: "30", forty: "40", fifty: "50", sixty: "60", seventy: "70", eighty: "80", ninety: "90", hundred: "100", quarter: "0.25" }; // Create reverse mapping for UOM lookup const uomReverseMapping = {}; Object.entries(uomMappings).forEach(([standardUom, variations]) => { variations.forEach(variation => { uomReverseMapping[variation] = standardUom; }); }); // Create regex pattern for all UOM variations (sorted by length desc to match longer ones first) const allUomVariations = Object.keys(uomReverseMapping).sort((a, b) => b.length - a.length); const uomPattern = allUomVariations.map(uom => uom.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); let qty = ""; let uom = ""; let product = ""; let price = ""; // --- IMPROVED PRICE EXTRACTION --- // First, look for specific price patterns with actual numbers const specificPricePatterns = [ /(?:₹|rs\.?|inr|rupees)\s*(\d+(?:\.\d+)?)/i, // ₹100, rs 100, inr 100, rupees 100 /(\d+(?:\.\d+)?)\s*(?:₹|rs\.?|inr|rupees)/i, // 100₹, 100 rs, 100 inr, 100 rupees /price\s*(?:is\s*)?(?:₹|rs\.?|inr|rupees)?\s*(\d+(?:\.\d+)?)/i, // price is 100, price rs 100 /costs?\s*(?:₹|rs\.?|inr|rupees)?\s*(\d+(?:\.\d+)?)/i, // costs 100, cost rs 100 /for\s*(?:₹|rs\.?|inr|rupees)?\s*(\d+(?:\.\d+)?)/i // for 100, for rs 100 ]; // Check for specific price patterns first for (const pattern of specificPricePatterns) { const priceMatch = cleanText.match(pattern); if (priceMatch && priceMatch[1]) { price = priceMatch[1]; break; } } // Remove the entire price phrase from text for further processing let textWithoutPrice = cleanText; if (price) { // Remove the matched price pattern for (const pattern of specificPricePatterns) { textWithoutPrice = textWithoutPrice.replace(pattern, " "); } } // Also remove currency indicators that don't have specific prices (like "in rupees", "for money") const currencyIndicators = [ /\bin\s+rupees?\b/gi, /\bfor\s+money\b/gi, /\bfor\s+rupees?\b/gi, /\bwith\s+rupees?\b/gi, /\bcurrency\s+rupees?\b/gi ]; currencyIndicators.forEach(pattern => { textWithoutPrice = textWithoutPrice.replace(pattern, " "); }); // Clean up extra spaces textWithoutPrice = textWithoutPrice.replace(/\s+/g, " ").trim(); // --- REST OF EXTRACTION LOGIC (same as before but using textWithoutPrice) --- // Pattern 1: Handle attached quantity-UOM (like "apple1kg", "milk2l", "water500ml") const attachedPattern = new RegExp(`(.+?)(\\d+(?:\\.\\d+)?)(${uomPattern})(?:\\s|$)`, 'i'); const attachedMatch = textWithoutPrice.match(attachedPattern); if (attachedMatch) { const productPart = attachedMatch[1].trim(); const qtyPart = attachedMatch[2]; const uomPart = attachedMatch[3]; if (productPart && qtyPart && uomReverseMapping[uomPart]) { product = productPart; qty = qtyPart; uom = uomReverseMapping[uomPart]; } } // Pattern 2: Spaced quantity-UOM patterns (like "apple 1 kg", "milk 2 liters", "water 500 ml") if (!qty || !uom) { const spacedPatterns = [ new RegExp(`(.+?)\\s+(\\d+(?:\\.\\d+)?)\\s+(${uomPattern})(?:\\s|$)`, 'i'), new RegExp(`(\\d+(?:\\.\\d+)?)\\s+(${uomPattern})\\s+(.+)`, 'i'), new RegExp(`(\\d+(?:\\.\\d+)?)\\s+(${uomPattern})\\s+of\\s+(.+)`, 'i') ]; for (const pattern of spacedPatterns) { const match = textWithoutPrice.match(pattern); if (match) { if (match[3] && !match[1].match(/^\d/)) { // Pattern: "product qty uom" product = match[1].trim(); qty = match[2]; uom = uomReverseMapping[match[3]]; } else if (match[3]) { // Pattern: "qty uom product" or "qty uom of product" qty = match[1]; uom = uomReverseMapping[match[2]]; product = match[3].trim(); } if (product && qty && uom) break; } } } // Pattern 3: Word number patterns (like "two kg apple", etc.) if (!qty || !uom) { const sortedNumberWords = Object.keys(numberWords).sort((a, b) => b.length - a.length); for (const numberPhrase of sortedNumberWords) { if (textWithoutPrice.includes(numberPhrase)) { const escapedPhrase = numberPhrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (!qty || !uom) { const wordNumbers = Object.keys(numberWords).join('|'); const wordNumberPatterns = [ new RegExp(`(.+?)\\s+(${wordNumbers})\\s+(${uomPattern})(?:\\s|$)`, 'i'), new RegExp(`(${wordNumbers})\\s+(${uomPattern})\\s+(.+)`, 'i'), new RegExp(`(${wordNumbers})\\s+(${uomPattern})\\s+of\\s+(.+)`, 'i') ]; for (const pattern of wordNumberPatterns) { const match = textWithoutPrice.match(pattern); if (match) { if (match[3] && !numberWords[match[1]]) { product = match[1].trim(); qty = numberWords[match[2]]; uom = uomReverseMapping[match[3]]; } else if (match[3]) { qty = numberWords[match[1]]; uom = uomReverseMapping[match[2]]; product = match[3].trim(); } if (product && qty && uom) break; } } } const compoundPatterns = [ new RegExp(`(.+?)\\s+(${escapedPhrase})\\s+(${uomPattern})(?:\\s|$)`, 'i'), new RegExp(`(${escapedPhrase})\\s+(${uomPattern})\\s+(.+)`, 'i'), new RegExp(`(${escapedPhrase})\\s+(${uomPattern})\\s+of\\s+(.+)`, 'i') ]; for (const pattern of compoundPatterns) { const match = textWithoutPrice.match(pattern); if (match) { if (match[3] && match[1] !== numberPhrase) { product = match[1].trim(); qty = numberWords[numberPhrase]; uom = uomReverseMapping[match[3]]; } else if (match[3]) { qty = numberWords[numberPhrase]; uom = uomReverseMapping[match[2]]; product = match[3].trim(); } if (product && qty && uom) break; } } if (product && qty && uom) break; } } } // Pattern 4: Fallback - look for any quantity and UOM separately if (!qty) { const qtyMatch = textWithoutPrice.match(/(\d+(?:\.\d+)?)/); if (qtyMatch) { qty = qtyMatch[1]; } else { const sortedNumberWords = Object.keys(numberWords).sort((a, b) => b.length - a.length); for (const numberPhrase of sortedNumberWords) { if (textWithoutPrice.includes(numberPhrase)) { qty = numberWords[numberPhrase]; break; } } } } if (!uom) { const words = textWithoutPrice.split(/\s+/); for (const word of words) { if (uomReverseMapping[word]) { uom = uomReverseMapping[word]; break; } } } // Extract product if not found yet if (!product) { const words = textWithoutPrice.split(/\s+/); const excludeWords = new Set(); if (qty) excludeWords.add(qty); if (uom) { excludeWords.add(uom); if (uomMappings[uom]) { uomMappings[uom].forEach(variation => excludeWords.add(variation)); } } Object.keys(numberWords).forEach(word => { excludeWords.add(word); word.split(' ').forEach(subword => excludeWords.add(subword)); }); Object.keys(uomReverseMapping).forEach(variation => excludeWords.add(variation)); ['of', 'and', 'the', 'a', 'an'].forEach(word => excludeWords.add(word)); const productWords = words.filter(word => !excludeWords.has(word) && !excludeWords.has(word.toLowerCase()) && !/^\d+(\.\d+)?$/.test(word) ); product = productWords.join(' ').trim(); } // Clean up product name product = product .replace(/^\W+|\W+$/g, '') .split(/\s+/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); // Set default quantity if product exists but no quantity found const finalQty = product && !qty ? "1" : qty; const result = { product: product || "", qty: finalQty || "", uom: uom || "", price: price || "" }; return result; }; // Setup speech recognition (same as before) useEffect(() => { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { setError("Speech Recognition not supported in this browser"); return; } const recognition = new SpeechRecognition(); recognition.lang = "en-US"; recognition.continuous = false; recognition.interimResults = false; recognition.maxAlternatives = 3; recognition.onstart = () => { setIsProcessing(false); setError(""); }; recognition.onresult = (event) => { setIsProcessing(true); const results = event.results[0]; let bestResult = results[0]; for (let i = 1; i < results.length; i++) { if (results[i].confidence > bestResult.confidence) { bestResult = results[i]; } } const spokenText = bestResult.transcript; const confidence = bestResult.confidence || 0; setTranscript(spokenText); setConfidence(confidence); const { product, qty, uom, price } = extractEntities(spokenText); setFormData({ product, qty, uom, price }); if (setVoiceData) { setVoiceData({ product, qty, uom, price, transcript: spokenText, confidence }); } setIsProcessing(false); }; recognition.onerror = (event) => { setIsProcessing(false); setError(event.error); setIsListening(false); }; recognition.onend = () => { setIsListening(false); setIsProcessing(false); }; recognitionRef.current = recognition; return () => { recognitionRef.current?.abort(); }; }, [setVoiceData]); useEffect(() => { if (error) { setMessageType("error"); setMessageData(`Recognition error: ${error}`); } }, [error]); const startListening = async () => { try { setError(""); const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); stream.getTracks().forEach((track) => track.stop()); recognitionRef.current?.start(); setIsListening(true); setTranscript(""); setFormData({ product: "", qty: "", uom: "" }); setConfidence(0); } catch { setError("Microphone access denied or not available"); setIsListening(false); } }; const stopListening = () => { recognitionRef.current?.stop(); setIsListening(false); setIsProcessing(false); }; const onComplete = useCallback(() => { setMessageData(null); setMessageType(null); }, []); return ( Voice Input: {isListening ? "Stop Listening" : "Start Listening"}
Examples: } >
{isListening ? : } {isListening && Listening...}
{/* Debug info */} {/* {transcript && (
Heard: "{transcript}" → Product: "{formData.product}", Qty: "{formData.qty}", UOM: "{formData.uom}", Price: "{formData.price}"
)} */}
); }; export default EnhancedVoiceForm;