From b58b291a0c79d78aae62ec36a2bc7f6cb4102123 Mon Sep 17 00:00:00 2001 From: sriram Date: Tue, 29 Sep 2026 16:44:03 +0530 Subject: [PATCH] Frontend Updates on Brand Discovery --- src/api/client.js | 11 ++++ src/pages/BrandDiscoveryPanel.jsx | 99 +++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/src/api/client.js b/src/api/client.js index 628760a..8552cf9 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -387,6 +387,17 @@ export const api = { body: JSON.stringify(payload), }), + // Web & retail listings: a background search (minutes, not seconds) that + // the panel starts, polls, and then reads through the preview's `use_web`. + startWebDiscovery: (brand, { refresh = false } = {}) => + request('/api/admin/brand-discovery/web-jobs', { + method: 'POST', + body: JSON.stringify({ brand, refresh }), + }), + + getWebDiscoveryJob: (jobId) => + request(`/api/admin/brand-discovery/web-jobs/${encodeURIComponent(jobId)}`), + ingestBrandDiscovery: ( brand, products, diff --git a/src/pages/BrandDiscoveryPanel.jsx b/src/pages/BrandDiscoveryPanel.jsx index 46643e8..16ce78a 100644 --- a/src/pages/BrandDiscoveryPanel.jsx +++ b/src/pages/BrandDiscoveryPanel.jsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { Search, Sparkles, Loader2, AlertTriangle, PlayCircle, Database, - ShieldCheck, PackageSearch, Barcode, + ShieldCheck, PackageSearch, Barcode, Store, } from 'lucide-react'; import { api } from '../api/client'; @@ -25,6 +25,14 @@ import { api } from '../api/client'; * The review step stays regardless. A person reading the list is still the last * check before anything is written, so the list is shown. * + * WEB & RETAIL LISTINGS (optional, off by default). For brands Open Food Facts + * does not carry - non-food above all - the server can search retailer listings + * (Blinkit, Zepto, Instamart, BigBasket, JioMart, Amazon, Flipkart). It reads + * only the search engine's result titles and links, never a shop's page and + * never a language model, and keeps a product only when a real listing names + * the brand and states a pack size. That search takes minutes, so it runs as a + * job: Discover starts it, this panel polls it, then asks for the preview. + * * There is no progress UI here on purpose. Ingest returns a BatchOut, and * AdminPage hands it to BatchCatalogPanel through the `adoptBatch` prop it * already has - which draws the eleven stages, and owns Resume and Cancel. @@ -32,16 +40,38 @@ import { api } from '../api/client'; const EVIDENCE_TONE = { openfacts: 'bg-emerald-500/10 text-emerald-700 border-emerald-500/20', + retail: 'bg-violet-500/10 text-violet-700 border-violet-500/20', catalog: 'bg-sky-500/10 text-sky-700 border-sky-500/20', registry: 'bg-amber-500/10 text-amber-700 border-amber-500/20', }; const EVIDENCE_LABEL = { openfacts: 'Open Food Facts', + retail: 'retail listing', catalog: 'already in catalog', registry: 'known sub-brand', }; +const WEB_POLL_MS = 2500; +const WEB_FINISHED = new Set(['done', 'partial', 'failed', 'interrupted']); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +// The badge for one row. A brand-store row has no `evidence` tier (the store IS +// the source), and used to fall through to "model only" - which it is not. +function evidenceBadge(product) { + if (product.evidence === 'retail' && product.retailer_count) { + const n = product.retailer_count; + return { tone: EVIDENCE_TONE.retail, label: `retail - ${n} shop${n === 1 ? '' : 's'}` }; + } + if (product.evidence) { + return { tone: EVIDENCE_TONE[product.evidence], label: EVIDENCE_LABEL[product.evidence] }; + } + if ((product.sources || []).includes('store')) { + return { tone: EVIDENCE_TONE.openfacts, label: "brand's own shop" }; + } + return null; +} + function Toggle({ checked, onChange, label, hint }) { return (