import { useState, useEffect, useRef, useCallback } from 'react'
import { Link, Route, Routes } from 'react-router-dom'
import JSZip from 'jszip'
import Gallery from './Gallery'
import ImageLightbox from './ImageLightbox'
const API_BASE = '/api'
const SESSION_STORAGE_KEY = 'hawbeez_last_run'
const AUTH_TOKEN_KEY = 'hawbeez_auth_token'
/** Use backend proxy for image URLs to avoid CORS when loading from R2. */
function proxyImageUrl(url) {
return url ? `${API_BASE}/proxy-image?url=${encodeURIComponent(url)}` : ''
}
const TARGET_AUDIENCE_OPTIONS = [
"Parents of toddlers (1–2.5 yrs)",
"Parents of 3–5 yrs",
"Gift buyers (kids)",
"Montessori / alternative education",
"Urban Tier 1 parents",
"Urban Tier 2 parents",
"First-time parents",
"Eco-conscious parents",
"Value-seeking parents (combos/deals)",
"Grandparents (gifting)",
"Screen-time worriers",
"Learning-through-play seekers",
"Educational toy buyers",
"Wooden toy fans",
"Birthday / party gifting",
"Festive gifting (Diwali, Children's Day)",
"COD / UPI-first",
"Instagram / Facebook parents",
"D2C toy buyers",
"₹1k–₹3k toy buyers",
"Repeat Hawbeez buyers",
"Desk organizer / school supplies",
"Bilingual (Hindi/English) families",
"Preschool / nursery parents",
"KG / early primary parents",
]
function authHeaders() {
const token = typeof localStorage !== 'undefined' ? localStorage.getItem(AUTH_TOKEN_KEY) : null
if (!token) return {}
return { Authorization: `Bearer ${token}` }
}
function LoginPage({ onLogin }) {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
async function handleSubmit(e) {
e.preventDefault()
setError(null)
setLoading(true)
try {
const res = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username.trim(), password }),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
setError(data.detail || 'Login failed')
return
}
if (data.access_token) {
localStorage.setItem(AUTH_TOKEN_KEY, data.access_token)
onLogin(data.access_token)
}
} catch (_) {
setError('Network error')
} finally {
setLoading(false)
}
}
return (
Ad Creative Studio — Hawbeez
Hawbeez
Eco-friendly Toys · Ad Creative Studio
Sign in
Use your Hawbeez studio account.
)
}
export default function App() {
const [token, setToken] = useState(() => (typeof localStorage !== 'undefined' ? localStorage.getItem(AUTH_TOKEN_KEY) : null))
const [url, setUrl] = useState('')
const [loading, setLoading] = useState(false)
const [streamStep, setStreamStep] = useState(null)
const [error, setError] = useState(null)
const [productData, setProductData] = useState(null)
const [analysis, setAnalysis] = useState(null)
const [adCreatives, setAdCreatives] = useState(null)
const [selectedForAds, setSelectedForAds] = useState([])
const [imageModels, setImageModels] = useState([])
const [selectedImageModel, setSelectedImageModel] = useState('nano-banana')
const [generatedAds, setGeneratedAds] = useState([])
const [generatingAds, setGeneratingAds] = useState(false)
const [generatingAdsTotal, setGeneratingAdsTotal] = useState(0)
const [generateAdsError, setGenerateAdsError] = useState(null)
const [editedCreatives, setEditedCreatives] = useState({}) // creative id -> full edited creative object
const [referenceImageUrl, setReferenceImageUrl] = useState(null) // user custom/upload; null = use product image
const [selectedReferenceUrls, setSelectedReferenceUrls] = useState([]) // up to 3 product image URLs user selected for ad generation; order = ref order
const [targetAudiences, setTargetAudiences] = useState([]) // selected audience segments for analysis/creatives (multi-select)
const [targetAudienceOpen, setTargetAudienceOpen] = useState(false)
const targetAudienceDropdownRef = useRef(null)
const [hasStoredSession, setHasStoredSession] = useState(false)
const [storedSession, setStoredSession] = useState(null)
const [lightboxImage, setLightboxImage] = useState(null)
const [backendOpenAIReady, setBackendOpenAIReady] = useState(null) // null = not checked yet, true/false = from /api/health
const generatedAdsSectionRef = useRef(null)
const prevGeneratingRef = useRef(false)
// Close target audience dropdown on outside click
useEffect(() => {
if (!targetAudienceOpen) return
const el = targetAudienceDropdownRef.current
function handleClick(e) {
if (el && !el.contains(e.target)) setTargetAudienceOpen(false)
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [targetAudienceOpen])
const handleLogout = useCallback(() => {
localStorage.removeItem(AUTH_TOKEN_KEY)
setToken(null)
}, [])
// Computed for any code that still expects the old single-result shape
const result =
productData != null || analysis != null || (adCreatives && adCreatives.length > 0)
? {
product_data: productData,
analysis,
ad_creatives: adCreatives || [],
}
: null
useEffect(() => {
if (!adCreatives?.length) return
fetch(`${API_BASE}/image-models`, { headers: authHeaders() })
.then((r) => {
if (r.status === 401) { localStorage.removeItem(AUTH_TOKEN_KEY); setToken(null); return null }
return r.json()
})
.then((data) => {
if (!data) return
setImageModels(data.models || [])
if (data.default && !selectedImageModel) setSelectedImageModel(data.default)
})
.catch(() => {})
}, [adCreatives?.length])
// Check backend OpenAI config (so we can show a clear message before user clicks Generate).
// 404 or errors (e.g. old backend without /api/health) → leave null (don't show warning).
useEffect(() => {
fetch(`${API_BASE}/health`)
.then((r) => {
if (!r.ok) return null
return r.json()
})
.then((data) => {
if (data && typeof data.openai_configured === 'boolean') {
setBackendOpenAIReady(data.openai_configured)
}
// else leave backendOpenAIReady as null (unknown)
})
.catch(() => { /* ignore: backend may not have /api/health */ })
}, [])
// Restore session offer on load
useEffect(() => {
try {
const raw = sessionStorage.getItem(SESSION_STORAGE_KEY)
if (raw) {
const data = JSON.parse(raw)
if (data?.product_data && data?.ad_creatives?.length) {
setHasStoredSession(true)
setStoredSession(data)
}
}
} catch (_) {}
}, [])
// Persist run to session when we have product + analysis + creatives
useEffect(() => {
if (!productData || !analysis || !adCreatives?.length) return
try {
sessionStorage.setItem(
SESSION_STORAGE_KEY,
JSON.stringify({
product_data: productData,
analysis,
ad_creatives: adCreatives,
})
)
} catch (_) {}
}, [productData, analysis, adCreatives])
// Scroll generated ads section into view when generation completes
useEffect(() => {
if (prevGeneratingRef.current && !generatingAds && generatedAds.length > 0) {
generatedAdsSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
prevGeneratingRef.current = generatingAds
}, [generatingAds, generatedAds.length])
async function handleRun() {
const trimmed = url.trim()
if (!trimmed) {
setError('Please enter a Hawbeez product URL.')
return
}
setError(null)
setStreamStep(null)
setProductData(null)
setAnalysis(null)
setAdCreatives(null)
setSelectedForAds([])
setGeneratedAds([])
setGenerateAdsError(null)
setEditedCreatives({})
setReferenceImageUrl(null)
setSelectedReferenceUrls([])
setHasStoredSession(false)
setStoredSession(null)
try {
sessionStorage.removeItem(SESSION_STORAGE_KEY)
} catch (_) {}
setLoading(true)
try {
const res = await fetch(`${API_BASE}/run/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ url: trimmed, target_audience: targetAudiences.length ? targetAudiences : undefined }),
})
if (res.status === 401) {
localStorage.removeItem(AUTH_TOKEN_KEY)
setToken(null)
return
}
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.detail || res.statusText)
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n\n')
buffer = lines.pop() || ''
for (const chunk of lines) {
const dataLine = chunk.split('\n').find((l) => l.startsWith('data: '))
if (!dataLine) continue
try {
const payload = JSON.parse(dataLine.slice(6))
if (payload.event === 'step') {
setStreamStep(payload.message || payload.step)
} else if (payload.event === 'scrape_done') {
setProductData(payload.data)
} else if (payload.event === 'analysis_done') {
setAnalysis(payload.data)
} else if (payload.event === 'creatives_done') {
setAdCreatives(payload.data || [])
} else if (payload.event === 'done') {
setStreamStep(null)
} else if (payload.event === 'error') {
setError(payload.message || 'Something went wrong.')
setStreamStep(null)
}
} catch (_) {}
}
}
} catch (e) {
setError(e.message || 'Something went wrong.')
setStreamStep(null)
} finally {
setLoading(false)
}
}
if (!token) {
return setToken(t)} />
}
return (
{lightboxImage && (
setLightboxImage(null)}
/>
)}
Hawbeez
Eco-friendly Toys · Ad Creative Studio
} />
Scrape, analyze & generate creatives for Hawbeez
{hasStoredSession && storedSession && (
You have a previous session saved.
)}
{backendOpenAIReady === false && (
Backend is missing OPENAI_API_KEY. Set it in backend/.env and restart the backend.
)}
Paste a Hawbeez product page URL. We’ll scrape the product, run a deep marketing analysis, and generate ad creative packages (product & no-product) for Meta.