// FRD Step 3 — rule-based regional nose-style recommendation. Renders the
// /api/recommend payload as ranked flashcards, each with a free warp-preview
// thumbnail (the enhanced variant) and score chips.
const ScoreChip = ({ label, value, tone }) => (
{label}
{value}
);
const _compatTone = (c) => (c >= 70 ? "good" : c >= 50 ? "amber" : "red");
const _harmonyText = (h) => {
if (!h || h.value == null) return "—";
if (h.delta == null) return `${h.value}`;
const sign = h.delta >= 0 ? "+" : "";
return `${h.value} (${sign}${h.delta})`;
};
const StyleFlashcard = ({ ctx, style, rank }) => {
const { state, set } = ctx;
const [previewUrl, setPreviewUrl] = React.useState(null);
const enhanced = style.variants.enhanced;
React.useEffect(() => {
let alive = true;
api.warp(state.session.session_id, enhanced.warp)
.then((r) => { if (alive) setPreviewUrl(r.image_url); })
.catch(() => {});
return () => { alive = false; };
}, [style.key]);
return (
{style.most_natural_match &&
Most Natural Match
}
#{rank}
{previewUrl
?
:
warp preview…
}
{style.name}
{style.characteristics.map((c, i) => {c} )}
Default targets (aspirational)
{style.default_metrics.map((m, i) => {m} )}
{style.approximation &&
≈ {style.approximation}
}
set({ selectedStyle: style.key, selectedVariant: "enhanced", step: "reference" })}>
See reference variants →
set({ warp: enhanced.warp, preset: "Custom", step: "adjust" })}>
Use in Adjust
);
};
const RecommendStep = ({ ctx }) => {
const { state, set } = ctx;
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
React.useEffect(() => {
if (!state.session || !state.region || !state.gender) return;
if (state.recommendation) return;
setLoading(true);
api.recommend(state.session.session_id, state.region, state.gender)
.then((rec) => { set({ recommendation: rec, intensityProfile: rec.profile }); setError(null); })
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, [state.session && state.session.session_id, state.region, state.gender, state.recommendation]);
if (!state.session) {
return (
Add a portrait first
Go to step 1 to pick a region, biological gender and photo.
);
}
if (!state.region || !state.gender) {
return (
Region & gender needed
Step 1 needs a region and biological gender before styles can be recommended.
);
}
const rec = state.recommendation;
return (
Recommended styles · {state.region} · {state.gender}
{rec &&
intensity · {rec.profile} }
{loading &&
Scoring styles against your analysis…
}
{error &&
{error}
}
{rec && (
{rec.styles.map((s, i) => )}
)}
);
};
Object.assign(window, { ScoreChip, StyleFlashcard, RecommendStep });