add AI voice assistant, chat widget, inventory and pharmacy pages with API services

This commit is contained in:
2026-09-18 10:39:03 +05:30
parent a343bb0dfc
commit 6c43b92671
7 changed files with 123 additions and 29 deletions
+11 -11
View File
@@ -65,16 +65,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
@@ -1419,6 +1409,7 @@
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -1429,6 +1420,7 @@
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1575,6 +1567,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.44",
"caniuse-lite": "^1.0.30001806",
@@ -2548,6 +2541,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2574,6 +2568,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
@@ -2604,6 +2599,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2613,6 +2609,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2632,6 +2629,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -2722,7 +2720,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -2929,6 +2928,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.5",
+24 -4
View File
@@ -35,7 +35,7 @@ export const AIChatWidget = () => {
text: result.replyText || "Action completed."
}]);
handleActionCommand(result.actionCommand);
handleActionCommand(result.actionCommand, result.searchTerm, userText);
} catch (error) {
console.error("AI command error:", error);
setMessages(prev => [...prev, {
@@ -47,7 +47,29 @@ export const AIChatWidget = () => {
}
};
const handleActionCommand = (actionCommand: string) => {
const handleActionCommand = (actionCommand: string, apiSearchTerm?: string, originalText?: string) => {
// Extract search term from backend response or user input
let searchTerm = apiSearchTerm;
if (!searchTerm && originalText) {
const match = originalText.match(/(?:how much (?:quantity|stock) (?:in|of)|quantity of|stock of|search for|search|find|do we have|is there any)\s+([a-zA-Z0-9\s\-_]+)/i);
if (match && match[1]) {
searchTerm = match[1].trim();
}
}
if (actionCommand === 'NAVIGATE_PHARMACY' || actionCommand === 'SEARCH_MEDICINE' || searchTerm) {
if (searchTerm) {
navigate(`/pharmacy?search=${encodeURIComponent(searchTerm)}`);
setTimeout(() => {
window.dispatchEvent(new CustomEvent('setPharmacySearch', { detail: searchTerm }));
window.dispatchEvent(new CustomEvent('setSearchQuery', { detail: searchTerm }));
}, 200);
} else {
navigate('/pharmacy');
}
return;
}
if (!actionCommand || actionCommand === 'NONE') return;
if (actionCommand === 'VIEW_LOW_STOCK') {
@@ -62,8 +84,6 @@ export const AIChatWidget = () => {
}, 500);
} else if (actionCommand === 'NAVIGATE_DASHBOARD') {
navigate('/');
} else if (actionCommand === 'NAVIGATE_PHARMACY') {
navigate('/pharmacy');
} else if (actionCommand === 'NAVIGATE_INVENTORY') {
navigate('/inventory');
setTimeout(() => {
+20 -3
View File
@@ -95,7 +95,26 @@ export const VoiceAssistant: React.FC = () => {
setAiResponse("I didn't quite understand that.");
}
if (result.actionCommand === 'VIEW_LOW_STOCK') {
// Extract search term from backend response or transcript
let searchTerm = result.searchTerm;
if (!searchTerm) {
const match = command.match(/(?:how much (?:quantity|stock) (?:in|of)|quantity of|stock of|search for|search|find|do we have|is there any)\s+([a-zA-Z0-9\s\-_]+)/i);
if (match && match[1]) {
searchTerm = match[1].trim();
}
}
if (result.actionCommand === 'NAVIGATE_PHARMACY' || result.actionCommand === 'SEARCH_MEDICINE' || searchTerm) {
if (searchTerm) {
navigate(`/pharmacy?search=${encodeURIComponent(searchTerm)}`);
setTimeout(() => {
window.dispatchEvent(new CustomEvent('setPharmacySearch', { detail: searchTerm }));
window.dispatchEvent(new CustomEvent('setSearchQuery', { detail: searchTerm }));
}, 200);
} else {
navigate('/pharmacy');
}
} else if (result.actionCommand === 'VIEW_LOW_STOCK') {
navigate('/inventory');
setTimeout(() => {
const event = new CustomEvent('setFilter', { detail: 'low-stock' });
@@ -109,8 +128,6 @@ export const VoiceAssistant: React.FC = () => {
}, 500);
} else if (result.actionCommand === 'NAVIGATE_DASHBOARD') {
navigate('/');
} else if (result.actionCommand === 'NAVIGATE_PHARMACY') {
navigate('/pharmacy');
} else if (result.actionCommand === 'NAVIGATE_INVENTORY') {
navigate('/inventory');
setTimeout(() => {
+7
View File
@@ -314,11 +314,18 @@ const Inventory = () => {
setIsAdjustModalOpen(true);
}
};
const handleSetSearch = (e: any) => {
if (typeof e.detail === 'string') {
setSearchQuery(e.detail);
}
};
window.addEventListener('setFilter', handleSetFilter);
window.addEventListener('openAdjustModal', handleOpenAdjust);
window.addEventListener('setSearchQuery', handleSetSearch);
return () => {
window.removeEventListener('setFilter', handleSetFilter);
window.removeEventListener('openAdjustModal', handleOpenAdjust);
window.removeEventListener('setSearchQuery', handleSetSearch);
};
}, []);
+58 -8
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Search, AlertCircle, Package, Layers, Plus, X, ChevronDown } from 'lucide-react';
import { createColumnHelper } from '@tanstack/react-table';
import { Table } from '../components/Table';
@@ -73,6 +74,8 @@ const columns = [
];
const Pharmacy = () => {
const location = useLocation();
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(25);
const [inventory, setInventory] = useState<PharmacyItem[]>([]);
@@ -246,18 +249,52 @@ const Pharmacy = () => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('action') === 'dispense') {
setIsDispenseModalOpen(true);
// Clean up URL after opening
window.history.replaceState({}, '', '/pharmacy');
}
}, []);
// Filter inventory based on search query
// Listen for external search triggers (e.g., Voice Assistant or Chat Widget)
useEffect(() => {
const handleSearchEvent = (e: any) => {
if (typeof e.detail === 'string') {
setSearchQuery(e.detail);
}
};
window.addEventListener('setPharmacySearch', handleSearchEvent);
window.addEventListener('setSearchQuery', handleSearchEvent);
return () => {
window.removeEventListener('setPharmacySearch', handleSearchEvent);
window.removeEventListener('setSearchQuery', handleSearchEvent);
};
}, []);
// Automatically sync search query when navigating with ?search=query
useEffect(() => {
const params = new URLSearchParams(location.search);
const searchParam = params.get('search');
if (searchParam !== null) {
setSearchQuery(searchParam);
}
}, [location.search]);
// Filter inventory based on search query with smart matching
const filteredInventory = useMemo(() => {
return inventory.filter(item =>
item.medicine.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.subtitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.manufacture.toLowerCase().includes(searchQuery.toLowerCase())
);
if (!searchQuery.trim()) return inventory;
const cleanSearch = searchQuery.trim().toLowerCase();
const compactSearch = cleanSearch.replace(/[\s\-_]+/g, '');
return inventory.filter(item => {
const med = item.medicine.toLowerCase();
const sub = item.subtitle.toLowerCase();
const mfr = item.manufacture.toLowerCase();
const compactMed = med.replace(/[\s\-_]+/g, '');
return med.includes(cleanSearch) ||
sub.includes(cleanSearch) ||
mfr.includes(cleanSearch) ||
compactMed.includes(compactSearch);
});
}, [inventory, searchQuery]);
// Reset pagination when search changes
@@ -337,8 +374,21 @@ const Pharmacy = () => {
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search medicines, OTC, or prescriptions..."
className="w-full py-3 bg-slate-50 border border-slate-200 hover:border-slate-300 hover:bg-slate-100/50 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 shadow-sm placeholder:text-slate-400 transition-all text-slate-800"
style={{ height: '46px', paddingLeft: '44px', paddingRight: '16px', fontSize: '15px' }}
style={{ height: '46px', paddingLeft: '44px', paddingRight: searchQuery ? '40px' : '16px', fontSize: '15px' }}
/>
{searchQuery && (
<button
onClick={() => {
setSearchQuery('');
navigate('/pharmacy', { replace: true });
}}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-slate-400 hover:text-slate-600 transition-colors"
title="Clear search"
type="button"
>
<X size={18} />
</button>
)}
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/v1';
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080').trim().replace(/\/+$/, '');
// Create a base Axios instance for API calls
export const api = axios.create({
+2 -2
View File
@@ -17,8 +17,8 @@
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},