import React, { useState, useEffect, useContext, createContext } from 'react'; import { Home, Upload, List, ShoppingCart, TrendingUp, Calendar, Trash2, Edit2, Save, X } from 'lucide-react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, BarChart, Bar, PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; const StoreContext = createContext(); const StoreProvider = ({ children }) => { const [products, setProducts] = useState(() => { const stored = localStorage.getItem('grocery-tracker-storage'); return stored ? JSON.parse(stored) : []; }); useEffect(() => { localStorage.setItem('grocery-tracker-storage', JSON.stringify(products)); }, [products]); const addProduct = (product) => { setProducts(prev => [...prev, { ...product, id: Date.now() }]); }; const updateProduct = (id, product) => { setProducts(prev => prev.map(p => p.id === id ? { ...p, ...product } : p)); }; const deleteProduct = (id) => { setProducts(prev => prev.filter(p => p.id !== id)); }; const clearProducts = () => { setProducts([]); }; return ( {children} ); }; const useStore = () => { const context = useContext(StoreContext); if (!context) { throw new Error('useStore must be used within a StoreProvider'); } return context; }; const MISTRAL_API_KEY = process.env.REACT_APP_MISTRAL_API_KEY; const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions'; const convertImageToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); }; const extractTextFromImage = async (file, userApiKey) => { const apiKeyToUse = userApiKey || MISTRAL_API_KEY; try { const base64Image = await convertImageToBase64(file); const response = await fetch(MISTRAL_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKeyToUse}` }, body: JSON.stringify({ model: 'pixtral-12b-2409', messages: [ { role: 'user', content: [ { type: 'text', text: 'Extract all text from this receipt image. Please provide the raw text exactly as it appears, maintaining the original structure and formatting.' }, { type: 'image_url', image_url: { url: base64Image } } ] } ], max_tokens: 1000, temperature: 0.1 }) }); if (!response.ok) { throw new Error(`Mistral API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { console.error('Error extracting text from image:', error); throw new Error('Failed to extract text from image'); } }; const processReceiptText = async (text, userApiKey) => { const apiKeyToUse = userApiKey || MISTRAL_API_KEY; try { const response = await fetch(MISTRAL_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKeyToUse}` }, body: JSON.stringify({ model: 'mistral-large-latest', messages: [ { role: 'user', content: `Analyze this receipt text and extract product information. Return a JSON array with objects containing: name, price, store (with name and address), and date. Receipt text: ${text} Please return ONLY a valid JSON array in this format: [ { "name": "Product Name", "price": 2.50, "store": { "name": "Store Name", "address": "Store Address" }, "date": "MM/DD/YYYY" } ]` } ], max_tokens: 2000, temperature: 0.1 }) }); if (!response.ok) { throw new Error(`Mistral API error: ${response.status} ${response.statusText}`); } const data = await response.json(); const jsonResponse = data.choices[0].message.content; const jsonMatch = jsonResponse.match(/\[[\s\S]*\]/); if (!jsonMatch) { throw new Error('Invalid JSON response from Mistral'); } const products = JSON.parse(jsonMatch[0]); return products.map(product => ({ ...product, price: parseFloat(product.price) || 0 })); } catch (error) { console.error('Error processing receipt text:', error); throw new Error('Failed to process receipt text'); } }; const UploadTab = () => { const { addProduct } = useStore(); const [file, setFile] = useState(null); const [loading, setLoading] = useState(false); const [step, setStep] = useState('upload'); const [extractedText, setExtractedText] = useState(''); const [processedProducts, setProcessedProducts] = useState([]); const [error, setError] = useState(null); const [apiKey, setApiKey] = useState(''); const handleFileSelect = (e) => { const selectedFile = e.target.files[0]; if (selectedFile && selectedFile.type.startsWith('image/')) { setFile(selectedFile); setStep('upload'); setError(null); } }; const handleUpload = async () => { if (!file) return; if (!apiKey && !MISTRAL_API_KEY) { setError('Please enter your Mistral API key or configure REACT_APP_MISTRAL_API_KEY environment variable.'); return; } setLoading(true); setError(null); setStep('extracting'); try { const text = await extractTextFromImage(file, apiKey); setExtractedText(text); setStep('processing'); const products = await processReceiptText(text, apiKey); setProcessedProducts(products); setStep('review'); } catch (error) { console.error('Error processing receipt:', error); setError(error.message); setStep('upload'); } finally { setLoading(false); } }; const handleSaveProducts = () => { processedProducts.forEach(product => addProduct(product)); setFile(null); setExtractedText(''); setProcessedProducts([]); setStep('upload'); setError(null); alert('Products saved successfully'); }; return (

Upload Receipt

{error && (

{error}

)} {step === 'upload' && (
setApiKey(e.target.value)} placeholder="Enter your Mistral API key" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />

Required to process receipt images

{file && (

Selected file: {file.name}

)}
{file && ( )}
)} {step === 'extracting' && (

Extracting text from image...

)} {step === 'processing' && (

Processing products...

)} {step === 'review' && (

Detected Products

Extracted Text:

                {extractedText}
              
{processedProducts.length > 0 ? ( processedProducts.map((product, index) => (
{product.name} ({product.store?.name || 'Unknown Store'})
${product.price.toFixed(2)}
)) ) : (

No products detected

)}
)}
); }; const Sidebar = ({ activeTab, setActiveTab }) => { const tabs = [ { id: 'home', label: 'Home', icon: Home }, { id: 'upload', label: 'Upload', icon: Upload }, { id: 'list', label: 'List', icon: List } ]; return (

Grocery Tracker

); }; const HomeTab = () => { const { products } = useStore(); const totalSpent = products.reduce((sum, p) => sum + p.price, 0); const productsByStore = products.reduce((acc, p) => { const key = p.store.name; if (!acc[key]) acc[key] = []; acc[key].push(p); return acc; }, {}); const priceVariations = products.reduce((acc, p) => { if (!acc[p.name]) acc[p.name] = []; acc[p.name].push(p); return acc; }, {}); const productsWithVariation = Object.entries(priceVariations) .filter(([_, prods]) => prods.length > 1) .map(([name, prods]) => ({ name, minPrice: Math.min(...prods.map(p => p.price)), maxPrice: Math.max(...prods.map(p => p.price)), difference: Math.max(...prods.map(p => p.price)) - Math.min(...prods.map(p => p.price)) })) .sort((a, b) => b.difference - a.difference) .slice(0, 5); const spendingByStore = Object.entries(productsByStore).map(([name, prods]) => ({ name, spending: prods.reduce((sum, p) => sum + p.price, 0), products: prods.length })); const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']; return (

Dashboard

Total Spent

${totalSpent.toFixed(2)}

Registered Products

{products.length}

Stores

{Object.keys(productsByStore).length}

{spendingByStore.length > 0 && (

Spending by Store

[`$${value.toFixed(2)}`, 'Spending']} />

Spending Distribution

`${name} ${(percent * 100).toFixed(0)}%`} outerRadius={80} fill="#8884d8" dataKey="spending" > {spendingByStore.map((entry, index) => ( ))} [`$${value.toFixed(2)}`, 'Spending']} />
)} {productsWithVariation.length > 0 && (

Products with Highest Price Variation

{productsWithVariation.map((product, index) => (
{product.name}
${product.minPrice.toFixed(2)} - ${product.maxPrice.toFixed(2)} +${product.difference.toFixed(2)}
))}
)}
); }; const ListTab = () => { const { products, updateProduct, deleteProduct } = useStore(); const [editingId, setEditingId] = useState(null); const [editForm, setEditForm] = useState({}); const startEdit = (product) => { setEditingId(product.id); setEditForm(product); }; const saveEdit = () => { updateProduct(editingId, editForm); setEditingId(null); setEditForm({}); }; const cancelEdit = () => { setEditingId(null); setEditForm({}); }; const handleDelete = (id) => { if (window.confirm('Are you sure you want to delete this product?')) { deleteProduct(id); } }; return (

Product List

{products.map((product) => ( ))}
Product Price Store Date Actions
{editingId === product.id ? ( setEditForm({...editForm, name: e.target.value})} className="w-full px-3 py-1 border border-gray-300 rounded" /> ) : (
{product.name}
)}
{editingId === product.id ? ( setEditForm({...editForm, price: parseFloat(e.target.value)})} className="w-full px-3 py-1 border border-gray-300 rounded" /> ) : (
${product.price.toFixed(2)}
)}
{product.store.name}
{product.store.address}
{editingId === product.id ? ( setEditForm({...editForm, date: e.target.value})} className="w-full px-3 py-1 border border-gray-300 rounded" /> ) : ( product.date )} {editingId === product.id ? ( <> ) : ( <> )}
{products.length === 0 && (

No products registered

)}
); }; const App = () => { const [activeTab, setActiveTab] = useState('home'); const renderContent = () => { switch (activeTab) { case 'home': return ; case 'upload': return ; case 'list': return ; default: return ; } }; return (
{renderContent()}
); }; export default App;