/* Studio 76 Atelier — Handbag Bench. Upload a handbag photo (or a sketch) → AI spec, construction flat, front·side·interior plate, designer illustration, and "see it carried" on a model, plus variations, reforms and guided edits. Talks only to handbag.php (keys server-side). */ const WEAR = [ { k: 'hand', label: 'In hand' }, { k: 'shoulder', label: 'On shoulder' }, { k: 'crossbody', label: 'Crossbody' }, { k: 'elbow', label: 'Elbow / editorial' }, { k: 'full', label: 'Full outfit' }, { k: 'street', label: 'Street style' }, { k: 'flatlay', label: 'Flat-lay' }, ]; const MODELS = [ { k: 'woman', label: 'On her' }, { k: 'man', label: 'On him' }, ]; const LOOKS = [ { k: 'editorial', label: 'Editorial' }, { k: 'indian', label: 'Indian' }, { k: 'millennial', label: 'Millennial' }, { k: 'glam', label: 'Glam' }, { k: 'studio', label: 'Studio' }, { k: 'goldenhour', label: 'Golden hour' }, ]; const FORMS = [ { k: 'tote', label: 'As a tote' }, { k: 'clutch', label: 'As a clutch' }, { k: 'crossbody', label: 'As a crossbody' }, { k: 'shoulder', label: 'As a shoulder bag' }, { k: 'backpack', label: 'As a backpack' }, { k: 'mini', label: 'As a mini bag' }, ]; /* Guided-edit vocab — discrete controls, each = one precise change on the zoomed render. */ const SHAPES = ['Tote', 'Clutch', 'Crossbody', 'Top-handle', 'Bucket', 'Hobo', 'Satchel', 'Baguette']; const MATERIALS = ['Full-grain', 'Saffiano', 'Pebbled', 'Suede', 'Patent', 'Canvas', 'Woven', 'Croc-embossed']; const HARDWARES = ['Gold', 'Silver', 'Gunmetal', 'Tonal']; const STRAPS = ['Top handle', 'Chain shoulder strap', 'Wide shoulder strap', 'Crossbody strap', 'None']; const COLORS = [ { name: 'Black', hex: '#1B130D' }, { name: 'Tan', hex: '#B98A56' }, { name: 'Cognac', hex: '#9A5B2C' }, { name: 'Cream', hex: '#EDE3CF' }, { name: 'Burgundy', hex: '#5E1F26' }, { name: 'Navy', hex: '#1C2B4A' }, { name: 'Forest', hex: '#26432F' }, { name: 'Chocolate', hex: '#3E2A1E' }, { name: 'Blush', hex: '#E7C3C0' }, { name: 'Red', hex: '#8E1B2E' }, ]; const DETAILS = [ { k: 'piping', op: 'add', params: { element: 'contrast piping along the panel seams' }, label: '+ Piping' }, { k: 'plaque', op: 'add', params: { element: 'a subtle metal logo plaque on the front' }, label: '+ Logo plaque' }, { k: 'feet', op: 'add', params: { element: 'protective metal feet on the base' }, label: '+ Base feet' }, { k: 'nostrap', op: 'remove', params: { element: 'the shoulder strap' }, label: '– Strap' }, ]; const SEL0 = { size: null, shape: null, material: null, color: null, hardware: null, strap: null, details: [], note: '' }; function cap1(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; } /* aspect of a generated key, for the refine (perfect-it) pass */ function aspectFor(key) { if (key === 'views') return '3:2'; if (key.slice(0, 4) === 'mag_') return '3:4'; if (key.slice(0, 2) === 'w_') return '4:5'; return '1:1'; } async function benchApi(action, payload) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 95000); let r; try { r = await fetch('handbag.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action }, payload)), signal: ctrl.signal }); } catch (e) { clearTimeout(timer); throw new Error(e && e.name === 'AbortError' ? 'That took too long — the studio is busy. Please try again.' : 'Network hiccup — please try again.'); } clearTimeout(timer); let j = null; try { j = await r.json(); } catch (e) {} if (!r.ok || !j || j.error) throw new Error((j && j.error) || ('The studio is busy right now (' + r.status + ') — please try again.')); return j; } function downscale(file, max) { return new Promise((res, rej) => { const fr = new FileReader(); fr.onerror = () => rej(new Error('read')); fr.onload = () => { const img = new Image(); img.onload = () => { const s = Math.min(1, max / Math.max(img.width, img.height)); const w = Math.round(img.width * s), h = Math.round(img.height * s); const c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(img, 0, 0, w, h); res(c.toDataURL('image/jpeg', 0.85)); }; img.onerror = () => rej(new Error('img')); img.src = fr.result; }; fr.readAsDataURL(file); }); } function isUrl(x) { return typeof x === 'string' && x.slice(0, 5) === 'data:'; } function Bench() { const [stage, setStage] = React.useState('idle'); const [entry, setEntry] = React.useState('piece'); const [src, setSrc] = React.useState(null); const [id, setId] = React.useState(null); const [spec, setSpec] = React.useState(null); const [out, setOut] = React.useState({}); const [look, setLook] = React.useState('editorial'); const [model, setModel] = React.useState('woman'); const [shots, setShots] = React.useState([]); const [morphs, setMorphs] = React.useState([]); const [fixes, setFixes] = React.useState([]); const [fixN, setFixN] = React.useState(0); const [varN, setVarN] = React.useState(0); const [varText, setVarText] = React.useState(''); const [saving, setSaving] = React.useState(false); const [savedN, setSavedN] = React.useState(''); const [savedUrl, setSavedUrl] = React.useState(''); const [drag, setDrag] = React.useState(false); const [err, setErr] = React.useState(''); const [urlText, setUrlText] = React.useState(''); const [modalSrc, setModalSrc] = React.useState(''); const [modalKey, setModalKey] = React.useState(''); const [sel, setSel] = React.useState(SEL0); const [cap, setCap] = React.useState(null); const [copied, setCopied] = React.useState(false); const fileRef = React.useRef(null); const savedSetRef = React.useRef(null); const savingRef = React.useRef(false); const aspectsRef = React.useRef({}); async function ingestRealize(srcPayload) { const a1 = await benchApi('analyze', srcPayload); if (!a1.id) throw new Error('Could not read that sketch — please try again.'); const r = await benchApi('realize', { id: a1.id }); setSrc(r.dataUrl); const a2 = await benchApi('analyze', { image: r.dataUrl }); if (!a2.id) throw new Error('Could not finish reading — please try again.'); setId(a2.id); setSpec(a2.spec); } function resetForIngest(sketch) { setErr(''); setEntry(sketch ? 'sketch' : 'piece'); setStage('analyzing'); setOut({}); setSpec(null); setId(null); setShots([]); setMorphs([]); setFixes([]); setVarN(0); setSavedUrl(''); savedSetRef.current = null; } async function handleFile(file, sketch) { if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; } resetForIngest(sketch); try { const dataUrl = await downscale(file, 1280); setSrc(dataUrl); if (sketch) { await ingestRealize({ image: dataUrl }); } else { const j = await benchApi('analyze', { image: dataUrl }); if (!j.id) throw new Error('Could not read that — please try again.'); setId(j.id); setSpec(j.spec); } setStage('ready'); } catch (e) { setErr(e.message || 'Could not read that image.'); setStage('idle'); } } async function handleUrl(url, sketch) { url = (url || '').trim(); if (!/^https?:\/\//i.test(url)) { setErr('Paste a valid image URL.'); return; } resetForIngest(sketch); setSrc(url); try { if (sketch) { await ingestRealize({ imageUrl: url }); } else { const j = await benchApi('analyze', { imageUrl: url }); if (!j.id) throw new Error('Could not read that — please try again.'); setId(j.id); setSpec(j.spec); } setStage('ready'); } catch (e) { setErr(e.message || 'Could not bring that image in.'); setStage('idle'); } } React.useEffect(() => { const u = new URLSearchParams(window.location.search).get('img'); if (u) { setUrlText(u); handleUrl(u); } }, []); async function gen(key, action, payload) { if (!id) return; setOut(o => Object.assign({}, o, { [key]: 'loading' })); try { const j = await benchApi(action, Object.assign({ id }, payload)); setOut(o => Object.assign({}, o, { [key]: j.dataUrl })); } catch (e) { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Generation failed.'); } } function genWear(place) { const w = WEAR.find(x => x.k === place), l = LOOKS.find(x => x.k === look); const key = 'w_' + place + '_' + look + '_' + model; const lookLabel = l.label + (model === 'man' ? ' · Him' : ''); if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: w.label, lookLabel }].concat(prev)); gen(key, 'wear', { place, look, model }); } function genMagazine() { const l = LOOKS.find(x => x.k === look); const key = 'mag_' + look + '_' + model; const lookLabel = l.label + (model === 'man' ? ' · Him' : ''); if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: 'Magazine cover', lookLabel }].concat(prev)); gen(key, 'magazine', { look, model }); } function genEditSet(ops, label) { const srcKey = modalKey, base = out[srcKey]; if (!id || !srcKey || !isUrl(base) || !ops.length) return; const aspect = aspectsRef.current[srcKey] || aspectFor(srcKey); const n = fixN + 1; setFixN(n); const key = 'fix_' + n; aspectsRef.current[key] = aspect; setFixes(prev => [{ key, label }].concat(prev)); closeModal(); setOut(o => Object.assign({}, o, { [key]: 'loading' })); benchApi('edit', { id, image: base, ops, aspect }) .then(j => setOut(o => Object.assign({}, o, { [key]: j.dataUrl }))) .catch(e => { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Edit failed.'); }); } function applyEdits() { const ops = [], parts = []; if (sel.size) { ops.push({ op: 'size', params: { dir: sel.size } }); parts.push(sel.size === 'up' ? 'Larger' : 'Smaller'); } if (sel.shape) { ops.push({ op: 'shape', params: { to: sel.shape } }); parts.push(cap1(sel.shape)); } if (sel.material) { ops.push({ op: 'material', params: { to: sel.material } }); parts.push(cap1(sel.material)); } if (sel.color) { ops.push({ op: 'color', params: { to: sel.color } }); parts.push(cap1(sel.color)); } if (sel.hardware) { ops.push({ op: 'hardware', params: { to: sel.hardware } }); parts.push(cap1(sel.hardware) + ' hw'); } if (sel.strap) { ops.push({ op: 'strap', params: { to: sel.strap } }); parts.push(cap1(sel.strap)); } sel.details.forEach(k => { const d = DETAILS.find(x => x.k === k); if (d) { ops.push({ op: d.op, params: d.params }); parts.push(d.label); } }); const note = sel.note.trim(); if (note) { ops.push({ op: 'note', params: { text: note } }); parts.push('“' + (note.length > 24 ? note.slice(0, 24) + '…' : note) + '”'); } if (!ops.length) return; let label = parts.slice(0, 3).join(' · '); if (parts.length > 3) label += ' +' + (parts.length - 3); genEditSet(ops, label); } function closeModal() { setModalSrc(''); setModalKey(''); setCap(null); setCopied(false); setSel(SEL0); } function capFull(c) { return (c.caption || '') + '\n\n' + (c.hashtags || []).map(h => '#' + String(h).replace(/^#/, '').replace(/\s+/g, '')).join(' '); } function loadCaption() { if (!id) return; setCap('loading'); setCopied(false); benchApi('caption', { id }).then(j => setCap(j)).catch(e => { setCap(null); setErr(e.message || 'Could not write a caption.'); }); } function copyCap(c) { const t = capFull(c); const done = () => { setCopied(true); setTimeout(() => setCopied(false), 1800); }; try { navigator.clipboard.writeText(t).then(done, done); } catch (e) { done(); } } function genVar() { if (!id) return; const n = varN + 1; setVarN(n); const text = varText.trim(); const key = 'var_' + n; const label = text ? ('“' + (text.length > 42 ? text.slice(0, 42) + '…' : text) + '”') : ('Variation ' + n); setMorphs(prev => [{ key, label }].concat(prev)); gen(key, 'variation', { n, instructions: text }); setVarText(''); } function genForm(form) { const f = FORMS.find(x => x.k === form), key = 'form_' + form; if (!morphs.find(m => m.key === key)) setMorphs(prev => [{ key, label: f.label }].concat(prev)); gen(key, 'reform', { form }); } async function save() { if (!id || saving || savingRef.current) return; savingRef.current = true; setSaving(true); setSavedUrl(''); setSavedN(''); setErr(''); try { const start = await benchApi('save_start', { id, spec, original: isUrl(src) ? src : '', setid: savedSetRef.current || undefined }); savedSetRef.current = start.setid; const items = []; if (isUrl(out.blueprint)) items.push({ key: 'blueprint', label: 'Construction flat', url: out.blueprint }); if (isUrl(out.views)) items.push({ key: 'views', label: 'Front · Side · Interior', url: out.views }); if (isUrl(out.realize)) items.push({ key: 'realize', label: 'Made real', url: out.realize }); if (isUrl(out.rendering)) items.push({ key: 'rendering', label: 'Designer illustration', url: out.rendering }); shots.forEach(s => { if (isUrl(out[s.key])) items.push({ key: s.key, label: s.placeLabel + ' · ' + s.lookLabel, url: out[s.key] }); }); morphs.forEach(m => { if (isUrl(out[m.key])) items.push({ key: m.key, label: m.label, url: out[m.key] }); }); fixes.forEach(f => { if (isUrl(out[f.key])) items.push({ key: f.key, label: 'Perfected · ' + f.label, url: out[f.key] }); }); let n = 0; for (const it of items) { await benchApi('save_item', { setid: start.setid, key: it.key, label: it.label, dataUrl: it.url }); n++; setSavedN(n + '/' + items.length); } try { const k = 'studio76_bag_sets', arr = JSON.parse(localStorage.getItem(k) || '[]').filter(s => s.id !== start.setid); arr.unshift({ id: start.setid, url: start.url, title: (spec && spec.bag_type) || 'Saved bag', n: items.length, created: Date.now() }); localStorage.setItem(k, JSON.stringify(arr.slice(0, 60))); } catch (e) {} setSavedUrl(start.url); } catch (e) { setErr(e.message || 'Save failed.'); } savingRef.current = false; setSaving(false); } function reset() { setStage('idle'); setSrc(null); setId(null); setSpec(null); setOut({}); setShots([]); setMorphs([]); setFixes([]); setFixN(0); setModalSrc(''); setModalKey(''); setCap(null); setCopied(false); setSel(SEL0); aspectsRef.current = {}; setVarN(0); setSaving(false); setSavedN(''); setSavedUrl(''); savedSetRef.current = null; savingRef.current = false; setErr(''); } const dims = spec && spec.estimated_dimensions_cm ? [spec.estimated_dimensions_cm.width, spec.estimated_dimensions_cm.height, spec.estimated_dimensions_cm.depth].filter(Boolean).join(' × ') : ''; const selCount = (sel.size ? 1 : 0) + (sel.shape ? 1 : 0) + (sel.material ? 1 : 0) + (sel.color ? 1 : 0) + (sel.hardware ? 1 : 0) + (sel.strap ? 1 : 0) + sel.details.length + (sel.note.trim() ? 1 : 0); const Row = ({ l, v }) => v ?
{l}{v}
: null; const Tile = ({ keyName, title, sub }) => { const v = out[keyName]; return (
{v === 'loading' ?
{sub || 'Working…'}
: (typeof v === 'string') ? {title} { setModalSrc(v); setModalKey(keyName); setCap(null); setCopied(false); setSel(SEL0); }} /> :
{sub}
}
{title} {(typeof v === 'string' && v !== 'loading') && Download}
); }; return (
Studio 76 AtelierApparelSaved looksJewellery✦ Handbag Bench

From a photo to a handbag.

Upload any bag — a photo, a screenshot, the one you spotted and could never find again. Our atelier reads it, drafts a construction flat, breaks down the spec, and shows it carried on a model, made by hand.

{stage === 'idle' && (
)} {stage === 'idle' && (
fileRef.current && fileRef.current.click()} onDragOver={e => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={e => { e.preventDefault(); setDrag(false); handleFile(e.dataTransfer.files[0], entry === 'sketch'); }}>

{entry === 'sketch' ? 'Drop your sketch here, or ' : 'Drop a photo here, or '}browse

{entry === 'sketch' ? 'A drawing, doodle or concept — we’ll render it in fine leather.' : 'JPG / PNG · a clear, well-lit shot works best'}

handleFile(e.target.files[0], entry === 'sketch')} />
)} {stage === 'idle' && (
setUrlText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUrl(urlText, entry === 'sketch'); }} />
)} {stage === 'idle' && (

✦ Clip a bag from Pinterest in one tap ↗

)} {err &&

{err}

} {stage !== 'idle' && (
{savedUrl ? View / share your set ↗ : Stores the spec, flat & every render at a shareable link.}
✦ See it carried — Instagram-ready

Pick who carries it and a look, then tap a shot. Mix & match to build a campaign.

{MODELS.map(m => ( ))} {LOOKS.map(l => ( ))}
{WEAR.map(w => { const k = 'w_' + w.k + '_' + look + '_' + model; return ( ); })} {(() => { const k = 'mag_' + look + '_' + model; return ( ); })()}
{shots.length > 0 && (
{shots.map(s => )}
)}
✦ Make it a collection — variations & forms

Tell us what to change, or just spin off a fresh take — then reimagine it as a matching tote, clutch, crossbody…

setVarText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') genVar(); }} />
{FORMS.map(f => { const k = 'form_' + f.k; return ; })}
{morphs.length > 0 && (
{morphs.map(m => )}
)}
{fixes.length > 0 && (
✦ Perfected takes

Refined renders — zoom any image and tell the atelier what to change to make more.

{fixes.map(f => )}
)}
)} {modalSrc && (
×
e.stopPropagation()}>
{modalKey && isUrl(out[modalKey]) && (
✦ Refine this design

Stack as many changes as you like, then Apply — they’re built in a single pass, the rest of the bag held steady. The new take lands in “Perfected takes”.

Size
Shape
{SHAPES.map(c => { const v = c.toLowerCase(); return ; })}
Material
{MATERIALS.map(st => { const v = st.toLowerCase(); return ; })}
Colour
{COLORS.map(c => { const v = c.name.toLowerCase(); return ; })}
Hardware
{HARDWARES.map(m => { const v = m.toLowerCase(); return ; })}
Strap / handle
{STRAPS.map(m => { const v = m.toLowerCase(); return ; })}
Detailing
{DETAILS.map(d => )}
Anything else (optional)
setSel(s => ({ ...s, note: e.target.value }))} onKeyDown={e => { if (e.key === 'Enter') applyEdits(); }} />
{selCount > 0 && }
)} {spec && (
✦ Post to Instagram {!cap &&

We’ll write the caption & hashtags — download the image and post it in seconds.

} {!cap && } {cap === 'loading' &&

Writing a caption…

} {cap && cap !== 'loading' && (