+
+
+
onModeChange('search')} />
onModeChange('chat')} />
-
- {health && health.status !== 'ok' && (
-
-
- {!health.database && !health.ollama
- ? 'DB & Ollama unreachable'
- : !health.database
- ? 'Database unreachable'
- : 'Ollama unreachable'}
-
- )}
-
{
+ e.preventDefault();
+ onSubmit();
+ }}
+ className="relative flex-1 w-full"
>
-
Stores
-
-
-
Analytics
-
-
-
Nutrition
-
-
-
Admin
-
-
+
+
onQueryChange(e.target.value)}
+ placeholder="Search catalog products — e.g. 'Recommend Paneer under ₹150', 'How many products in Cadbury?'"
+ className="w-full rounded-full border border-ink-900/10 bg-white py-2 pl-10 pr-4 text-xs text-ink-950 placeholder:text-slate-400 focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/20 shadow-xs"
+ />
+
+ )}
-
- {mode === 'search' && (
-
- )}
-
+
);
}
@@ -83,11 +36,11 @@ function ModeButton({ icon: Icon, label, active, onClick }) {
return (
);
}
diff --git a/frontend/src/components/UploadExcelModal.jsx b/frontend/src/components/UploadExcelModal.jsx
new file mode 100644
index 0000000..1293c2b
--- /dev/null
+++ b/frontend/src/components/UploadExcelModal.jsx
@@ -0,0 +1,276 @@
+import { useState, useRef } from 'react';
+import { X, UploadCloud, Download, FileSpreadsheet, CheckCircle2, AlertCircle, Loader2, Table } from 'lucide-react';
+import { api } from '../api/client';
+
+export function UploadExcelModal({ isOpen, onClose, tabType, tabTitle, onSuccess }) {
+ const [file, setFile] = useState(null);
+ const [previewRows, setPreviewRows] = useState(null);
+ const [isDragging, setIsDragging] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [result, setResult] = useState(null);
+ const [error, setError] = useState(null);
+ const fileInputRef = useRef(null);
+
+ if (!isOpen) return null;
+
+ const parsePreview = (selectedFile) => {
+ setResult(null);
+ setError(null);
+ setFile(selectedFile);
+
+ // Generate quick client-side text preview for CSV/TSV/Text
+ if (selectedFile.name.endsWith('.csv') || selectedFile.name.endsWith('.tsv') || selectedFile.name.endsWith('.txt')) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const text = e.target?.result || '';
+ const lines = text.split('\n').filter((l) => l.trim().length > 0);
+ if (lines.length > 0) {
+ const headers = lines[0].split(/,|\t/).map((h) => h.trim().replace(/^["']|["']$/g, ''));
+ const rows = lines.slice(1, 6).map((line) =>
+ line.split(/,|\t/).map((c) => c.trim().replace(/^["']|["']$/g, ''))
+ );
+ setPreviewRows({ headers, rows, totalLines: lines.length - 1 });
+ }
+ };
+ reader.readAsText(selectedFile.slice(0, 1024 * 10)); // read first 10KB
+ } else {
+ // Excel binary preview placeholder
+ setPreviewRows({ headers: ['Excel File Data Detected'], rows: [[selectedFile.name, `${(selectedFile.size / 1024).toFixed(1)} KB`]], totalLines: 'Excel' });
+ }
+ };
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ setIsDragging(true);
+ };
+
+ const handleDragLeave = () => {
+ setIsDragging(false);
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ setIsDragging(false);
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
+ const droppedFile = e.dataTransfer.files[0];
+ parsePreview(droppedFile);
+ }
+ };
+
+ const handleFileChange = (e) => {
+ if (e.target.files && e.target.files[0]) {
+ parsePreview(e.target.files[0]);
+ }
+ };
+
+ const handleUpload = async () => {
+ if (!file) return;
+ setLoading(true);
+ setError(null);
+ setResult(null);
+
+ try {
+ const res = await api.uploadFile(tabType, file);
+ setResult(res);
+ if (onSuccess) onSuccess(res);
+ } catch (err) {
+ setError(err.message || 'Failed to import file. Please check file format.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleClose = () => {
+ setFile(null);
+ setPreviewRows(null);
+ setResult(null);
+ setError(null);
+ onClose();
+ };
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+
+
+
+
+ Upload {tabTitle || 'Excel / CSV File'}
+
+
+ Import `.xlsx`, `.xls`, or `.csv` records into the database
+
+
+
+
+
+
+ {/* Template Download Option */}
+
+
+
Need a sample format?
+
Download pre-formatted CSV template with example columns.
+
+
+
+ Sample CSV
+
+
+
+ {/* Drag and Drop Zone */}
+
fileInputRef.current?.click()}
+ className={`mt-4 flex cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed p-6 text-center transition-all ${
+ isDragging
+ ? 'border-amber-500 bg-amber-500/10 scale-[1.01]'
+ : file
+ ? 'border-leaf-500/60 bg-leaf-500/5'
+ : 'border-ink-900/15 bg-white hover:border-amber-400 hover:bg-slate-50'
+ }`}
+ >
+
+
+
+
+
+
+ {file ? (
+
+
{file.name}
+
+ {(file.size / 1024).toFixed(1)} KB · Click or drag another file to change
+
+
+ ) : (
+
+
+ Click to browse or drag & drop Excel / CSV file
+
+
+ Supports `.xlsx`, `.xls`, `.csv`, `.tsv`
+
+
+ )}
+
+
+ {/* File Preview Table */}
+ {previewRows && (
+
+
+
+
+ Data Preview (First 5 Rows)
+
+ {typeof previewRows.totalLines === 'number' && (
+
+ {previewRows.totalLines} total data rows detected
+
+ )}
+
+
+
+
+
+
+ {previewRows.headers.map((h, idx) => (
+ | {h} |
+ ))}
+
+
+
+ {previewRows.rows.map((row, rIdx) => (
+
+ {row.map((cell, cIdx) => (
+ | {cell} |
+ ))}
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Error Alert */}
+ {error && (
+
+ )}
+
+ {/* Success Alert */}
+ {result && (
+
+
+
+
{result.message}
+
+ File `{result.filename}` ({result.rows_imported} rows) imported successfully.
+
+
+
+ )}
+
+ {/* Action Buttons */}
+
+
+
+ {!result && (
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/AdminPage.jsx b/frontend/src/pages/AdminPage.jsx
index 487e564..92ca531 100644
--- a/frontend/src/pages/AdminPage.jsx
+++ b/frontend/src/pages/AdminPage.jsx
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
-import { ArrowLeft, PlayCircle, Loader2, CheckCircle2, XCircle, Clock } from 'lucide-react';
+import { ArrowLeft, PlayCircle, Loader2, CheckCircle2, XCircle, Clock, Wrench } from 'lucide-react';
import { api } from '../api/client';
+import { NavigationHeader } from '../components/NavigationHeader';
const STATUS_STYLES = {
pending: { icon: Clock, className: 'text-slate-500 bg-ink-100' },
@@ -53,10 +54,14 @@ export function AdminPage() {
}
return (
-
-
-
Back to catalog
-
+
+
+
+
Catalog ingestion
@@ -127,6 +132,7 @@ export function AdminPage() {
})}
)}
+
);
}
diff --git a/frontend/src/pages/AnalyticsPage.jsx b/frontend/src/pages/AnalyticsPage.jsx
index 3ba17ec..41f556d 100644
--- a/frontend/src/pages/AnalyticsPage.jsx
+++ b/frontend/src/pages/AnalyticsPage.jsx
@@ -1,12 +1,14 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useCallback } from 'react';
import { Link } from 'react-router-dom';
import {
BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis,
Tooltip, ResponsiveContainer, CartesianGrid, Legend,
} from 'recharts';
-import { Flame, Trophy, TrendingUp, Award } from 'lucide-react';
+import { Flame, Trophy, TrendingUp, Award, BarChart3 as BarChartIcon, UploadCloud } from 'lucide-react';
import { api } from '../api/client';
import { Spinner, EmptyState } from '../components/Atoms';
+import { NavigationHeader } from '../components/NavigationHeader';
+import { UploadExcelModal } from '../components/UploadExcelModal';
const PALETTE = ['#e2a33d', '#3f8556', '#8b2e3c', '#2c3e63', '#c2872a'];
@@ -20,6 +22,15 @@ export function AnalyticsPage() {
const [topProducts, setTopProducts] = useState([]);
const [topBy, setTopBy] = useState('revenue');
+ const [isUploadOpen, setIsUploadOpen] = useState(false);
+
+ const reloadData = useCallback(() => {
+ setComparisonLoading(true);
+ api.getStoreComparison().then(setComparison).catch(() => setComparison(null)).finally(() => setComparisonLoading(false));
+ setTrendingLoading(true);
+ api.getTrending({ window: window_, scope: 'overall', top_k: 10 }).then(setTrending).catch(() => setTrending([])).finally(() => setTrendingLoading(false));
+ api.getTopProducts({ by: topBy, order: 'top', limit: 8 }).then(setTopProducts).catch(() => setTopProducts([]));
+ }, [window_, topBy]);
useEffect(() => {
api.getStoreComparison().then(setComparison).catch(() => setComparison(null)).finally(() => setComparisonLoading(false));
@@ -39,7 +50,17 @@ export function AnalyticsPage() {
return (
-
+
+
+
+
+
{comparisonLoading ? (
@@ -189,6 +210,14 @@ export function AnalyticsPage() {
+
+
setIsUploadOpen(false)}
+ tabType="analytics"
+ tabTitle="Sales Analytics Data"
+ onSuccess={() => reloadData()}
+ />
);
}
@@ -218,20 +247,3 @@ function prettify(imageId) {
const parts = imageId.split('_').filter((p, i, arr) => !(i > 0 && arr[i - 1] === p));
return parts.join(' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
-
-function PageHeader() {
- return (
-
- );
-}
diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx
index 1330c4b..e860cf9 100644
--- a/frontend/src/pages/HomePage.jsx
+++ b/frontend/src/pages/HomePage.jsx
@@ -142,6 +142,12 @@ export function HomePage() {
onQueryChange={setQuery}
onSubmit={runSearch}
health={health}
+ onResetHome={() => {
+ setSelectedBrand(null);
+ setSelectedCategory(null);
+ setQuery('');
+ setMode('search');
+ }}
/>