const { useState, useRef, useEffect } = React;
// ============================================
// ICONS (Inline SVG for accessibility)
// ============================================
const Icons = {
Shield: () => (
),
Package: () => (
),
Home: () => (
),
Plus: () => (
),
X: () => (
),
Check: () => (
),
AlertCircle: () => (
),
Camera: () => (
),
User: () => (
),
Settings: () => (
),
Loader: () => (
)
};
// ============================================
// USER CONTEXT HOOK - Persistent Storage
// ============================================
function useUserContext() {
const [userContext, setUserContext] = useState(() => {
const saved = localStorage.getItem('gmtracker_user_context');
if (saved) {
try {
return JSON.parse(saved);
} catch {
return null;
}
}
return null;
});
const saveUserContext = (context) => {
setUserContext(context);
localStorage.setItem('gmtracker_user_context', JSON.stringify(context));
};
const clearUserContext = () => {
setUserContext(null);
localStorage.removeItem('gmtracker_user_context');
};
return { userContext, saveUserContext, clearUserContext };
}
// ============================================
// USER PROFILE SETUP COMPONENT
// ============================================
function UserProfileSetup({ userContext, onSave, onClose }) {
const [formData, setFormData] = useState(userContext || {
age: '',
location: '',
dietary_restrictions: [],
allergies: [],
health_conditions: [],
language_preference: 'english'
});
const [customAllergy, setCustomAllergy] = useState('');
const [customCondition, setCustomCondition] = useState('');
const [customDiet, setCustomDiet] = useState('');
const commonAllergies = ['Peanuts', 'Tree nuts', 'Milk', 'Eggs', 'Wheat', 'Soy', 'Fish', 'Shellfish', 'Sesame'];
const commonConditions = ['Diabetes', 'High blood pressure', 'Heart disease', 'Kidney disease', 'Liver disease', 'Pregnant', 'Breastfeeding', 'Asthma'];
const commonDiets = ['Vegetarian', 'Vegan', 'Gluten-free', 'Low sodium', 'Low sugar', 'Halal', 'Kosher'];
const toggleArrayItem = (array, item) => {
if (array.includes(item)) {
return array.filter(i => i !== item);
}
return [...array, item];
};
const handleAddCustom = (type, value, setter) => {
if (value.trim()) {
setFormData({
...formData,
[type]: [...formData[type], value.trim()]
});
setter('');
}
};
const handleSubmit = (e) => {
e.preventDefault();
const cleanedData = {
...formData,
age: formData.age ? parseInt(formData.age) : null
};
onSave(cleanedData);
};
return (
Your Profile
Help us personalize product recommendations for you
);
}
// ============================================
// FEATURE 1: SAFETY COACH COMPONENT
// ============================================
function SafetyCoach() {
const [isOpen, setIsOpen] = useState(false);
const [step, setStep] = useState(1);
const [symptom, setSymptom] = useState('');
const [context, setContext] = useState({
alcohol_recent: false,
caffeine_recent: false,
meal_within_1h: false
});
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const [liveRegion, setLiveRegion] = useState('');
const dialogRef = useRef(null);
const openButtonRef = useRef(null);
useEffect(() => {
if (isOpen && dialogRef.current) {
dialogRef.current.focus();
}
}, [isOpen]);
const handleOpen = () => {
setIsOpen(true);
setStep(1);
setSymptom('');
setContext({ alcohol_recent: false, caffeine_recent: false, meal_within_1h: false });
setResult(null);
setLiveRegion('Safety check dialog opened');
};
const handleClose = () => {
setIsOpen(false);
setResult(null);
setLiveRegion('Safety check dialog closed');
if (openButtonRef.current) {
openButtonRef.current.focus();
}
};
const handleCheckSafety = async () => {
setLoading(true);
setLiveRegion('Checking medication safety...');
try {
const response = await fetch('/api/safety/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symptom, context })
});
const data = await response.json();
setResult(data);
setStep(2);
setLiveRegion(`Safety result: ${data.status}. ${data.message}`);
} catch (error) {
setLiveRegion('Error checking safety. Please try again.');
console.error('Safety check error:', error);
} finally {
setLoading(false);
}
};
const handleLogDose = async () => {
if (!result) return;
setLiveRegion('Logging dose...');
try {
const doseMatch = result.message.match(/(\d+)\s*mg/);
const doseMg = doseMatch ? parseInt(doseMatch[1]) : 500;
const medMatch = result.message.match(/of\s+(\w+)/);
const medName = medMatch ? medMatch[1] : 'medication';
await fetch('/api/doses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item_name: medName,
amount_mg: doseMg,
source: 'safety_check'
})
});
setLiveRegion(`Logged ${doseMg} milligrams of ${medName}`);
setTimeout(handleClose, 1500);
} catch (error) {
setLiveRegion('Error logging dose');
console.error('Log dose error:', error);
}
};
if (!isOpen) {
return (
Medication Safety Coach
Get personalized guidance on what medications are safe to take right now based on your recent doses and health factors.
What's Safe to Take Right Now?
);
}
return (
{step === 1 ? 'Safety Check' : 'Safety Result'}
{step === 1 && (
)}
{step === 2 && result && (
Status: {result.status}
{result.message}
{result.recommendations && result.recommendations.length > 0 && (
Recommendations:
{result.recommendations.filter(r => r).map((rec, idx) => (
{rec}
))}
)}
{result.status === 'safe' && (
Log That I Took This
)}
Close
)}
{liveRegion}
);
}
// ============================================
// FEATURE 2: MY ITEMS (MEDICATION PROFILES)
// ============================================
function MyItems() {
const [items, setItems] = useState([]);
const [showAddForm, setShowAddForm] = useState(false);
const [loading, setLoading] = useState(false);
const [liveRegion, setLiveRegion] = useState('');
const [formData, setFormData] = useState({
display_name: '',
strength: '',
form: '',
indication_plain: '',
instructions_plain: '',
warnings_plain: ''
});
const formRef = useRef(null);
const addButtonRef = useRef(null);
useEffect(() => {
loadItems();
}, []);
useEffect(() => {
if (showAddForm && formRef.current) {
formRef.current.focus();
}
}, [showAddForm]);
const loadItems = async () => {
try {
const response = await fetch('/api/items');
const data = await response.json();
setItems(data);
setLiveRegion(`Loaded ${data.length} medication items`);
} catch (error) {
console.error('Error loading items:', error);
setLiveRegion('Error loading items');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setLiveRegion('Saving medication item...');
try {
const response = await fetch('/api/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
const newItem = await response.json();
setLiveRegion(`Saved new item: ${newItem.display_name}`);
setFormData({
display_name: '',
strength: '',
form: '',
indication_plain: '',
instructions_plain: '',
warnings_plain: ''
});
setShowAddForm(false);
loadItems();
if (addButtonRef.current) {
addButtonRef.current.focus();
}
}
} catch (error) {
console.error('Error saving item:', error);
setLiveRegion('Error saving item');
} finally {
setLoading(false);
}
};
return (
My Medications & Products
setShowAddForm(!showAddForm)}
className="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition-all flex items-center gap-2"
aria-label={showAddForm ? 'Cancel adding item' : 'Add new medication item'}
aria-expanded={showAddForm}
>
{showAddForm ? : }
{showAddForm ? 'Cancel' : 'Add Item'}
{showAddForm && (
)}
{items.length === 0 ? (
No medication items yet. Add your first item to get started.
) : (
items.map((item) => {
const ariaLabel = `${item.display_name}${item.strength ? ` ${item.strength}` : ''}${
item.form ? `, ${item.form}` : ''
}${item.indication_plain ? `. For ${item.indication_plain}` : ''}${
item.instructions_plain ? `. ${item.instructions_plain}` : ''
}`;
return (
{item.display_name} {item.strength && {item.strength} }
{item.form &&
{item.form}
}
{item.indication_plain && (
For: {item.indication_plain}
)}
{item.instructions_plain && (
Instructions: {item.instructions_plain}
)}
{item.warnings_plain && (
Warning: {item.warnings_plain}
)}
);
})
)}
{liveRegion}
);
}
// ============================================
// FEATURE 3: HOME INVENTORY
// ============================================
function HomeInventory() {
const [inventory, setInventory] = useState([]);
const [summary, setSummary] = useState(null);
const [showAddForm, setShowAddForm] = useState(false);
const [itemText, setItemText] = useState('');
const [isMedication, setIsMedication] = useState(false);
const [loading, setLoading] = useState(false);
const [liveRegion, setLiveRegion] = useState('');
const formRef = useRef(null);
const addButtonRef = useRef(null);
useEffect(() => {
loadInventory();
loadSummary();
}, []);
useEffect(() => {
if (showAddForm && formRef.current) {
formRef.current.focus();
}
}, [showAddForm]);
const loadInventory = async () => {
try {
const response = await fetch('/api/inventory');
const data = await response.json();
setInventory(data);
} catch (error) {
console.error('Error loading inventory:', error);
}
};
const loadSummary = async () => {
try {
const response = await fetch('/api/inventory/summary');
const data = await response.json();
setSummary(data);
setLiveRegion(`You have ${data.medication_count} medications and ${data.grocery_count} grocery items in your list`);
} catch (error) {
console.error('Error loading summary:', error);
}
};
const handleAddItems = async (e) => {
e.preventDefault();
setLoading(true);
setLiveRegion('Adding inventory items...');
const lines = itemText.split('\n').filter(line => line.trim());
const items = lines.map(line => ({
name: line.trim(),
type: isMedication ? 'medication' : 'grocery'
}));
try {
const response = await fetch('/api/inventory/batch_add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items })
});
if (response.ok) {
const result = await response.json();
setLiveRegion(`Added ${result.added_count} inventory items`);
setItemText('');
setIsMedication(false);
setShowAddForm(false);
loadInventory();
loadSummary();
if (addButtonRef.current) {
addButtonRef.current.focus();
}
}
} catch (error) {
console.error('Error adding items:', error);
setLiveRegion('Error adding items');
} finally {
setLoading(false);
}
};
const handleStatusChange = async (itemId, newStatus) => {
setLiveRegion(`Updating status to ${newStatus}...`);
try {
const response = await fetch(`/api/inventory/${itemId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
});
if (response.ok) {
setLiveRegion(`Status updated to ${newStatus}`);
loadInventory();
loadSummary();
}
} catch (error) {
console.error('Error updating status:', error);
setLiveRegion('Error updating status');
}
};
const getStatusColor = (status) => {
switch (status) {
case 'normal': return 'text-green-700 bg-green-50';
case 'low': return 'text-yellow-700 bg-yellow-50';
case 'out': return 'text-red-700 bg-red-50';
default: return 'text-gray-700 bg-gray-50';
}
};
const formatDate = (dateString) => {
const date = new Date(dateString);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'today';
if (diffDays === 1) return 'yesterday';
if (diffDays < 7) return `${diffDays} days ago`;
return date.toLocaleDateString();
};
return (
Home Inventory
setShowAddForm(!showAddForm)}
className="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition-all flex items-center gap-2"
aria-label={showAddForm ? 'Cancel adding items' : 'Add purchased items'}
aria-expanded={showAddForm}
>
{showAddForm ? : }
{showAddForm ? 'Cancel' : 'Add Purchased Items'}
{summary && (
Summary: {summary.medication_count} medications, {summary.grocery_count} groceries, {summary.total} total items
{summary.low_items.length > 0 && (
Low: {summary.low_items.join(', ')}
)}
{summary.out_items.length > 0 && (
Out: {summary.out_items.join(', ')}
)}
)}
{showAddForm && (
Add Purchased Items
List items you bought (one per line)
setItemText(e.target.value)}
className="w-full p-3 text-lg border-2 border-gray-300 rounded-lg focus:border-green-500"
placeholder="milk
bread
ibuprofen 200 mg"
rows={5}
required
aria-required="true"
/>
setIsMedication(e.target.checked)}
className="w-5 h-5"
/>
This list contains medications
{loading ? 'Adding...' : 'Add Items to Inventory'}
)}
{inventory.length === 0 ? (
No items in inventory yet. Add your first items to get started.
) : (
inventory.map((item) => {
const ariaLabel = `${item.name}, ${item.type}, status: ${item.status}, last purchased ${formatDate(item.last_purchased_at)}`;
return (
{item.name}
{item.type}
{item.status.toUpperCase()}
Purchased {formatDate(item.last_purchased_at)}
{item.linked_item && (
Linked to: {item.linked_item.display_name}
)}
Update status for {item.name}
handleStatusChange(item.id, e.target.value)}
className="p-2 border-2 border-gray-300 rounded-lg focus:border-blue-500 text-lg"
aria-label={ariaLabel}
>
OK
Low
Out
);
})
)}
{liveRegion}
);
}
// ============================================
// FEATURE 4: PRODUCT SCANNER (WITH USER CONTEXT)
// ============================================
function ProductScanner({ userContext, onOpenProfile }) {
const [scanning, setScanning] = useState(false);
const [result, setResult] = useState(null);
const [image, setImage] = useState(null);
const [scanType, setScanType] = useState('auto');
const [liveRegion, setLiveRegion] = useState('');
const fileInputRef = useRef(null);
const cameraInputRef = useRef(null);
const handleFileSelect = async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
const imageData = event.target.result;
setImage(imageData);
await scanImage(imageData);
};
reader.readAsDataURL(file);
};
const scanImage = async (imageData) => {
setScanning(true);
setResult(null); // Clear previous result
setLiveRegion('Scanning product with AI...');
try {
const base64Data = imageData.split(',')[1];
// Build request with user context
const requestBody = {
image: base64Data,
scan_type: scanType
};
// Add user context if available
if (userContext) {
requestBody.user_context = userContext;
}
console.log('Sending scan request...'); // Debug log
const response = await fetch('/api/scan/product', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
console.log('Response status:', response.status); // Debug log
if (!response.ok) {
const errorText = await response.text();
console.error('API Error:', errorText);
setResult({
success: false,
product_type: 'error',
spoken_description: `Server error: ${response.status}. Please try again.`,
warnings: [errorText.substring(0, 200)]
});
setLiveRegion('Scan failed due to server error.');
return;
}
const data = await response.json();
console.log('Scan result:', data); // Debug log
setResult(data);
if (data.success) {
setLiveRegion(`Scan successful: ${data.spoken_description}`);
speakText(data.spoken_description);
} else {
setLiveRegion('Scan failed. Please try again.');
}
} catch (error) {
console.error('Scan error:', error);
setLiveRegion('Error scanning image. Please try again.');
} finally {
setScanning(false);
}
};
const speakText = (text) => {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 0.9;
window.speechSynthesis.speak(utterance);
}
};
const handleReset = () => {
setImage(null);
setResult(null);
setLiveRegion('');
};
return (
Scan Product with AI
{/* User Profile Status */}
{userContext ? (
<>
Personalized Mode Active
{userContext.age ? `Age: ${userContext.age}` : ''}
{userContext.health_conditions?.length > 0 ? ` • ${userContext.health_conditions.length} health conditions` : ''}
{userContext.allergies?.length > 0 ? ` • ${userContext.allergies.length} allergies` : ''}
>
) : (
<>
Set up your profile for personalized tips
Get warnings specific to your health conditions & allergies
>
)}
{userContext ? 'Edit Profile' : 'Set Up Profile'}
Take a photo or upload an image of any medicine bottle or grocery item.
Gemini AI will identify it and read all details aloud for you.
{userContext && ' Results will be personalized based on your health profile.'}
Scan Type:
{['auto', 'medication', 'grocery'].map(type => (
setScanType(type)}
className={`p-3 rounded-lg border-2 transition-all ${
scanType === type
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-200 hover:border-blue-300'
}`}
>
{type}
))}
{!image ? (
) : (
{scanning ? (
Scanning with Gemini AI...
{userContext &&
Personalizing results for you...
}
) : result ? (
{result.success ? '✓ Scan Complete' : '✗ Scan Failed'}
{result.success && (
<>
Type: {result.product_type}
{result.product_name &&
Product: {result.product_name}
}
{result.brand &&
Brand: {result.brand}
}
{result.quantity &&
Quantity: {result.quantity}
}
{/* Key Point - Personalized Highlight */}
{result.key_point && (
Personalized Tip:
{result.key_point}
)}
{result.spoken_description}
{result.warnings && result.warnings.length > 0 && (
⚠️ Warnings:
{result.warnings.map((w, i) => (
{w}
))}
)}
{/* Speak Again Button */}
speakText(result.spoken_description)}
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-xl mb-3"
>
🔊 Read Aloud Again
>
)}
{!result.success && (
{result.spoken_description}
)}
) : null}
Scan Another Product
)}
{liveRegion}
);
}
// ============================================
// MAIN APP COMPONENT
// ============================================
function GMtrackerApp() {
const [activeTab, setActiveTab] = useState('dashboard');
const [showProfileSetup, setShowProfileSetup] = useState(false);
const { userContext, saveUserContext, clearUserContext } = useUserContext();
const handleSaveProfile = (context) => {
saveUserContext(context);
setShowProfileSetup(false);
};
return (
GMtracker - Grocery & Medicine Tracker
AI-powered accessibility tool for safe medication management
{/* Profile Button in Header */}
setShowProfileSetup(true)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all ${
userContext
? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
}`}
aria-label="Open profile settings"
>
{userContext ? 'Profile Set' : 'Set Profile'}
{[
{ id: 'dashboard', label: 'Dashboard', icon: },
{ id: 'scanner', label: 'Scan Product', icon: },
{ id: 'items', label: 'My Medications', icon: },
{ id: 'inventory', label: 'Home Inventory', icon: }
].map(tab => (
setActiveTab(tab.id)}
className={`flex items-center gap-2 px-6 py-4 font-semibold transition-all ${
activeTab === tab.id
? 'text-blue-600 border-b-4 border-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
}`}
>
{tab.icon}
{tab.label}
))}
{activeTab === 'dashboard' && (
<>
Quick Actions
setActiveTab('scanner')}
className="w-full text-left p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all flex items-center gap-3"
>
Scan a Product
Use AI to identify medicine or grocery
setActiveTab('items')}
className="w-full text-left p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all flex items-center gap-3"
>
View My Medications
Manage your medication profiles
setActiveTab('inventory')}
className="w-full text-left p-4 border-2 border-gray-200 rounded-lg hover:border-green-500 hover:bg-green-50 transition-all flex items-center gap-3"
>
Check Home Inventory
See what you have at home
{/* Profile Card */}
Your Health Profile
{userContext ? (
{userContext.age &&
Age: {userContext.age}
}
{userContext.location &&
Location: {userContext.location}
}
{userContext.health_conditions?.length > 0 && (
Health Conditions: {userContext.health_conditions.join(', ')}
)}
{userContext.allergies?.length > 0 && (
Allergies: {userContext.allergies.join(', ')}
)}
{userContext.dietary_restrictions?.length > 0 && (
Dietary: {userContext.dietary_restrictions.join(', ')}
)}
setShowProfileSetup(true)}
className="flex-1 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg"
>
Edit Profile
Clear
) : (
Set up your profile to get personalized product recommendations based on your age, health conditions, and allergies.
setShowProfileSetup(true)}
className="w-full bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-3 px-6 rounded-lg"
>
Set Up Profile
)}
{/* About Card */}
About GMtracker
Check medication safety in real-time
AI-powered product scanning with Gemini
Personalized tips based on your health profile
Fully accessible with screen readers
>
)}
{activeTab === 'scanner' && (
setShowProfileSetup(true)}
/>
)}
{activeTab === 'items' && }
{activeTab === 'inventory' && }
{/* Profile Setup Modal */}
{showProfileSetup && (
setShowProfileSetup(false)}
/>
)}
);
}
ReactDOM.render( , document.getElementById('root'));