/* Studio 76 — The Stylist. Upload an outfit → AI styles the jewellery that completes it.
Add your own instructions, render the ones you love, then see them on you.
Talks only to blueprint.php (style → style_render). */
async function api(action, payload) {
const r = await fetch('blueprint.php', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.assign({ action }, payload)) });
let j = {}; try { j = await r.json(); } catch (e) {}
if (!r.ok || j.error) throw new Error(j.error || ('Something went wrong (' + r.status + ')'));
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.86)); };
img.onerror = () => rej(new Error('img')); img.src = fr.result; };
fr.readAsDataURL(file);
});
}
const isUrl = x => typeof x === 'string' && x.slice(0, 5) === 'data:';
function Stylist() {
const [stage, setStage] = React.useState('idle'); // idle | analyzing | ready
const [src, setSrc] = React.useState(null);
const [id, setId] = React.useState(null);
const [outfit, setOutfit] = React.useState(null);
const [picks, setPicks] = React.useState([]);
const [renders, setRenders] = React.useState({}); // index → dataUrl | 'loading'
const [instr, setInstr] = React.useState('');
const [urlText, setUrlText] = React.useState('');
const [drag, setDrag] = React.useState(false);
const [modal, setModal] = React.useState('');
const [err, setErr] = React.useState('');
const fileRef = React.useRef(null);
async function handleFile(file) {
if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; }
setErr(''); setStage('analyzing'); setOutfit(null); setPicks([]); setRenders({}); setId(null);
try {
const d = await downscale(file, 1280); setSrc(d);
const j = await api('style', { image: d, instructions: instr });
setId(j.id); setOutfit(j.outfit); setPicks(j.picks || []); setStage('ready');
} catch (e) { setErr(e.message || 'Could not read that outfit.'); setStage('idle'); }
}
async function handleUrl(url) {
url = (url || '').trim();
if (!/^https?:\/\//i.test(url)) { setErr('Paste a valid image URL.'); return; }
setErr(''); setStage('analyzing'); setOutfit(null); setPicks([]); setRenders({}); setId(null); setSrc(url);
try { const j = await api('style', { imageUrl: url, instructions: instr }); setId(j.id); setOutfit(j.outfit); setPicks(j.picks || []); setStage('ready'); }
catch (e) { setErr(e.message || 'Could not bring that in.'); setStage('idle'); }
}
function renderPick(i) {
if (!id || renders[i] === 'loading') return;
setRenders(r => Object.assign({}, r, { [i]: 'loading' }));
api('style_render', { id, pick: picks[i], instructions: instr })
.then(j => setRenders(r => Object.assign({}, r, { [i]: j.dataUrl })))
.catch(e => { setRenders(r => { const n = Object.assign({}, r); delete n[i]; return n; }); setErr(e.message || 'Render failed.'); });
}
function renderAll() { picks.forEach((p, i) => { if (!renders[i]) renderPick(i); }); }
function tryOn(dataUrl) {
try { sessionStorage.setItem('s76_piece', dataUrl); } catch (e) {}
window.location.href = '/tryiton.html';
}
function reset() { setStage('idle'); setSrc(null); setId(null); setOutfit(null); setPicks([]); setRenders({}); setErr(''); }
const Row = ({ l, v }) => v ?
{l} {v}
: null;
return (
Style the whole look.
Show us what you’re wearing — a dress, a saree, a whole outfit — and we’ll style the jewellery that completes it. Render the pieces you love, then see them on you.
{err && {err}
}
{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]); }}>
Drop your outfit photo, or browse
A clear, well-lit shot of the look — on you or laid out — works best.
handleFile(e.target.files[0])} />
setInstr(e.target.value)} />
Optional — your instructions guide what we recommend & how it’s rendered.
setUrlText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUrl(urlText); }} />
handleUrl(urlText)} style={{padding:'12px 18px'}}>Bring it in
)}
{stage === 'analyzing' && (
)}
{stage === 'ready' && (
{src ?
: null}
New outfit
{outfit && (
✦ The read
{outfit.summary}
)}
setInstr(e.target.value)} />
Edit, then re-render any piece to apply.
Styled for your look
{picks.length > 0 && Render all ↻ }
{picks.map((p, i) => {
const r = renders[i];
return (
{p.type}
{isUrl(r)
?
setModal(r)} />
: r === 'loading'
?
Rendering…
:
renderPick(i)}>✦ Render it
}
{p.name}
{[p.style, p.metal, p.stones].filter(Boolean).join(' · ')}
{p.why &&
“{p.why}” }
{isUrl(r) && (
)}
);
})}
)}
setModal('')}>
×
{modal &&
e.stopPropagation()} />}
);
}
ReactDOM.createRoot(document.getElementById('root')).render( );