This commit is contained in:
sriram
2026-08-11 19:14:03 +05:30
commit 3765e666bc
35 changed files with 7024 additions and 0 deletions

183
src/api/client.js Normal file
View File

@@ -0,0 +1,183 @@
const BASE = import.meta.env.VITE_API_BASE_URL || '';
class ApiError extends Error {
constructor(message, status) {
super(message);
this.status = status;
}
}
async function request(path, options = {}) {
let res;
try {
res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
...options,
});
} catch {
throw new ApiError(
`Could not reach the API at ${BASE || '(same origin)'}${path}. Is the backend running ` +
`(uvicorn app.main:app) and reachable?`,
0
);
}
if (!res.ok) {
let detail = `Request failed (${res.status})`;
try {
const body = await res.json();
detail = body.detail || JSON.stringify(body);
} catch {
/* ignore parse errors, keep generic message */
}
throw new ApiError(detail, res.status);
}
if (res.status === 204) return null;
return res.json();
}
function qs(params = {}) {
const usp = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') usp.set(k, v);
});
const s = usp.toString();
return s ? `?${s}` : '';
}
// Browse endpoints intentionally request a very high `limit` explicitly
// (rather than relying on the backend's default) so that the UI always
// shows the FULL catalog for a brand / for "All brands", however many
// thousands of products that ends up being. Do not lower this to a small
// page size unless you also add real pagination controls to the grid -
// silently truncating the product list here was the exact bug this
// constant is guarding against.
const SHOW_ALL_PRODUCTS_LIMIT = 100000;
export const api = {
getHealth: () => request('/api/health'),
getBrands: () => request('/api/brands'),
getBrandCategories: (brand) => request(`/api/brands/${encodeURIComponent(brand)}/categories`),
getBrandProducts: (brand, { category, limit = SHOW_ALL_PRODUCTS_LIMIT, offset = 0 } = {}) =>
request(`/api/brands/${encodeURIComponent(brand)}/products${qs({ category, limit, offset })}`),
getAllProducts: ({ category, limit = SHOW_ALL_PRODUCTS_LIMIT, offset = 0 } = {}) =>
request(`/api/products${qs({ category, limit, offset })}`),
getProductDetail: (brand, imageId) =>
request(`/api/brands/${encodeURIComponent(brand)}/products/${encodeURIComponent(imageId)}`),
search: (q, { brand, category, top_k = 50 } = {}) =>
request(`/api/search${qs({ q, brand, category, top_k })}`),
chat: ({ query, brand, category, top_k, history }) =>
request('/api/chat', {
method: 'POST',
body: JSON.stringify({ query, brand, category, top_k, history }),
}),
generateCatalog: ({ brand, max_products = 50 }) =>
request('/api/catalog/generate', {
method: 'POST',
body: JSON.stringify({ brand, max_products }),
}),
getJobStatus: (jobId) => request(`/api/catalog/jobs/${jobId}`),
// --- Store Intelligence (v3.0) ---
getStores: () => request('/api/stores'),
getStore: (storeId) => request(`/api/stores/${encodeURIComponent(storeId)}`),
getStoreProducts: (storeId, { category, in_stock_only, limit = 500, offset = 0 } = {}) =>
request(`/api/stores/${encodeURIComponent(storeId)}/products${qs({ category, in_stock_only, limit, offset })}`),
getProductAcrossStores: (brand, imageId) =>
request(`/api/products/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}/stores`),
getStoreDiscounts: (storeId) => request(`/api/stores/${encodeURIComponent(storeId)}/discounts`),
getProductDiscount: (storeId, brand, imageId) =>
request(`/api/stores/${encodeURIComponent(storeId)}/discounts/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}`),
getTrending: ({ window = 'weekly', scope = 'overall', scope_value, top_k = 10 } = {}) =>
request(`/api/trending${qs({ window, scope, scope_value, top_k })}`),
getRecommendations: (brand, imageId, topK = 5) =>
request(`/api/recommendations/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}${qs({ top_k: topK })}`),
getStoreAnalytics: (storeId) => request(`/api/analytics/store/${encodeURIComponent(storeId)}`),
getStoreComparison: () => request('/api/analytics/compare'),
getProductAnalytics: (brand, imageId) => request(`/api/analytics/product/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}`),
getTopProducts: ({ by = 'revenue', order = 'top', limit = 10 } = {}) =>
request(`/api/analytics/top-products${qs({ by, order, limit })}`),
seedStoreIntelligence: ({ reset_orders = true, days = 90, seed = 42 } = {}) =>
request('/api/admin/store-intelligence/seed', { method: 'POST', body: JSON.stringify({ reset_orders, days, seed }) }),
trainStoreModels: ({ models } = {}) =>
request('/api/admin/store-intelligence/train', { method: 'POST', body: JSON.stringify({ models: models || null }) }),
getStoreIntelligenceJob: (jobId) => request(`/api/admin/store-intelligence/jobs/${jobId}`),
// --- AI Nutritional Intelligence Module ---
getNutrition: (brand, imageId) =>
request(`/api/nutrition/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}`),
getNutritionHealthScore: (brand, imageId) =>
request(`/api/nutrition/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}/health-score`),
getNutritionInsights: (brand, imageId) =>
request(`/api/nutrition/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}/insights`),
getNutritionAlternatives: (brand, imageId, topK = 5) =>
request(`/api/nutrition/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}/alternatives${qs({ top_k: topK })}`),
getNutritionSimilar: (brand, imageId, topK = 5) =>
request(`/api/nutrition/${encodeURIComponent(brand)}/${encodeURIComponent(imageId)}/similar${qs({ top_k: topK })}`),
compareNutrition: (products) => request(`/api/nutrition/compare${qs({ products: products.join(',') })}`),
getDietCompatibleProducts: (tag, { category, exclude_allergen, limit = 20, offset = 0 } = {}) =>
request(`/api/nutrition/diet/${encodeURIComponent(tag)}${qs({ category, exclude_allergen, limit, offset })}`),
getHighProteinProducts: ({ category, limit = 20, offset = 0 } = {}) =>
request(`/api/nutrition/high-protein${qs({ category, limit, offset })}`),
getLowSugarProducts: ({ category, limit = 20, offset = 0 } = {}) =>
request(`/api/nutrition/low-sugar${qs({ category, limit, offset })}`),
getHighFiberProducts: ({ category, limit = 20, offset = 0 } = {}) =>
request(`/api/nutrition/high-fiber${qs({ category, limit, offset })}`),
filterNutritionProducts: ({ sort_by = 'health_score', order = 'desc', category, diet_tag, exclude_allergen, limit = 20, offset = 0 } = {}) =>
request(`/api/nutrition/products${qs({ sort_by, order, category, diet_tag, exclude_allergen, limit, offset })}`),
getNutritionAnalyticsDashboard: (limit = 10) => request(`/api/nutrition/analytics/dashboard${qs({ limit })}`),
getNutritionLeaderboards: (limit = 10) => request(`/api/nutrition/analytics/leaderboards${qs({ limit })}`),
getNutritionRankings: (limit = 10) => request(`/api/nutrition/analytics/rankings${qs({ limit })}`),
getNutritionDistribution: () => request('/api/nutrition/analytics/distribution'),
getPersonalizedNutritionRecommendations: (customerId, topK = 8) =>
request(`/api/nutrition/recommendations/${encodeURIComponent(customerId)}${qs({ top_k: topK })}`),
enrichNutrition: ({ skip_if_verified = true, generate_narrative = true, max_products } = {}) =>
request('/api/admin/nutrition-intelligence/enrich', {
method: 'POST', body: JSON.stringify({ skip_if_verified, generate_narrative, max_products: max_products || null }),
}),
trainNutritionModels: () => request('/api/admin/nutrition-intelligence/train', { method: 'POST', body: JSON.stringify({}) }),
getNutritionIntelligenceJob: (jobId) => request(`/api/admin/nutrition-intelligence/jobs/${jobId}`),
getNutritionEnrichmentStatus: () => request('/api/admin/nutrition-intelligence/status'),
// --- Excel / CSV Upload API ---
uploadFile: async (tabType, file) => {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${BASE}/api/upload/${encodeURIComponent(tabType)}`, {
method: 'POST',
body: formData,
});
if (!res.ok) {
let detail = `Upload failed (${res.status})`;
try {
const body = await res.json();
detail = body.detail || JSON.stringify(body);
} catch {}
throw new ApiError(detail, res.status);
}
return res.json();
},
getTemplateUrl: (tabType) => `${BASE}/api/upload/template/${encodeURIComponent(tabType)}`,
};
export { ApiError };