Spaces:
Sleeping
feat(profile): KI-077 — Name field + show current captured profile in Build-your-profile panel
Browse filesUser screenshot: the "Build your profile" sheet showed empty defaults
(Just me / None / etc) even when the chat had captured the user's name,
age, dependents, city, etc. Also no name input — user can't introduce
themselves through the UI, only through chat.
Fix:
- backend/main.py — added `name` to `profile_dict` in BOTH /api/profile/
completeness AND /api/profile (POST) so the captured name flows to the
UI + the panel can save a new name. Added `name` to ProfileUpdateRequest
+ in the loop that applies request fields. On name set, also persists
to the named-profile JSON store (40-data/profiles/<persona_id>.json) so
a returning visitor's profile auto-loads next time.
- frontend/src/lib/api.ts — added `name` to UserProfile type.
- frontend/src/app/page.tsx — ProfileBuilderPanel:
• New `name` state + text input at top of the form with
"captured from chat" badge when initialProfile.name is set.
• useEffect to keep state in sync if initialProfile updates while
the panel is open (chat captures new fields → panel reflects them).
• Submit path includes name in postProfileUpdate.
Now the user can:
1. Open the profile panel and see what the chat has captured so far
(name, age, dependents, conditions, city, etc. all pre-selected).
2. Edit any field including their name.
3. Save → backend persists to session state + named-profile store →
next visit they're auto-recognised.
Verified: `npx tsc --noEmit` exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/main.py +12 -0
- frontend/src/app/page.tsx +49 -0
- frontend/src/lib/api.ts +1 -0
|
@@ -552,6 +552,7 @@ class ProfileCompletenessResponse(BaseModel):
|
|
| 552 |
|
| 553 |
class ProfileUpdateRequest(BaseModel):
|
| 554 |
session_id: str
|
|
|
|
| 555 |
age: Optional[int] = None
|
| 556 |
dependents: Optional[str] = None
|
| 557 |
income_band: Optional[str] = None
|
|
@@ -616,6 +617,7 @@ async def profile_update(req: ProfileUpdateRequest):
|
|
| 616 |
# Update only fields the client explicitly sent (non-None) — keeps partial
|
| 617 |
# save flows clean
|
| 618 |
for field_name in (
|
|
|
|
| 619 |
"age", "dependents", "income_band", "existing_cover_inr", "primary_goal",
|
| 620 |
"location_tier", "parents_to_insure", "parents_age_max", "parents_has_ped",
|
| 621 |
"health_conditions", "budget_band",
|
|
@@ -624,8 +626,18 @@ async def profile_update(req: ProfileUpdateRequest):
|
|
| 624 |
if v is not None:
|
| 625 |
setattr(sess.profile, field_name, v)
|
| 626 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
p = sess.profile
|
| 628 |
profile_dict = {
|
|
|
|
| 629 |
"age": p.age, "dependents": p.dependents, "income_band": p.income_band,
|
| 630 |
"existing_cover_inr": p.existing_cover_inr, "primary_goal": p.primary_goal,
|
| 631 |
"location_tier": p.location_tier, "parents_to_insure": p.parents_to_insure,
|
|
|
|
| 552 |
|
| 553 |
class ProfileUpdateRequest(BaseModel):
|
| 554 |
session_id: str
|
| 555 |
+
name: Optional[str] = None # KI-077
|
| 556 |
age: Optional[int] = None
|
| 557 |
dependents: Optional[str] = None
|
| 558 |
income_band: Optional[str] = None
|
|
|
|
| 617 |
# Update only fields the client explicitly sent (non-None) — keeps partial
|
| 618 |
# save flows clean
|
| 619 |
for field_name in (
|
| 620 |
+
"name", # KI-077 — accept name updates from the profile-builder UI
|
| 621 |
"age", "dependents", "income_band", "existing_cover_inr", "primary_goal",
|
| 622 |
"location_tier", "parents_to_insure", "parents_age_max", "parents_has_ped",
|
| 623 |
"health_conditions", "budget_band",
|
|
|
|
| 626 |
if v is not None:
|
| 627 |
setattr(sess.profile, field_name, v)
|
| 628 |
|
| 629 |
+
# KI-077 — if name is set, also persist to the named-profile store so a
|
| 630 |
+
# returning visitor's profile is recoverable across sessions.
|
| 631 |
+
if req.name:
|
| 632 |
+
try:
|
| 633 |
+
from backend.profile_store import save_profile
|
| 634 |
+
save_profile(req.name, sess.profile, session_id=req.session_id)
|
| 635 |
+
except Exception as e:
|
| 636 |
+
print(f"[profile_store] save failed for {req.name}: {type(e).__name__}: {e}")
|
| 637 |
+
|
| 638 |
p = sess.profile
|
| 639 |
profile_dict = {
|
| 640 |
+
"name": p.name, # KI-077
|
| 641 |
"age": p.age, "dependents": p.dependents, "income_band": p.income_band,
|
| 642 |
"existing_cover_inr": p.existing_cover_inr, "primary_goal": p.primary_goal,
|
| 643 |
"location_tier": p.location_tier, "parents_to_insure": p.parents_to_insure,
|
|
@@ -925,6 +925,10 @@ function ProfileBuilderPanel({
|
|
| 925 |
onClose: () => void;
|
| 926 |
uiLang: UILang;
|
| 927 |
}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 928 |
const [age, setAge] = useState<number | null>(initialProfile.age ?? null);
|
| 929 |
const [dependents, setDependents] = useState<string>(initialProfile.dependents ?? "self");
|
| 930 |
const [budget, setBudget] = useState<string>(initialProfile.budget_band ?? "");
|
|
@@ -937,6 +941,23 @@ function ProfileBuilderPanel({
|
|
| 937 |
const [parentsAgeMax, setParentsAgeMax] = useState<number | null>(initialProfile.parents_age_max ?? null);
|
| 938 |
const [busy, setBusy] = useState(false);
|
| 939 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
const hindi = uiLang === "hi";
|
| 941 |
|
| 942 |
const toggleCondition = (c: string) => {
|
|
@@ -955,6 +976,7 @@ function ProfileBuilderPanel({
|
|
| 955 |
try {
|
| 956 |
const resp = await postProfileUpdate({
|
| 957 |
session_id: sid,
|
|
|
|
| 958 |
age: age ?? undefined,
|
| 959 |
dependents: dependents || undefined,
|
| 960 |
budget_band: budget || undefined,
|
|
@@ -1000,6 +1022,33 @@ function ProfileBuilderPanel({
|
|
| 1000 |
</div>
|
| 1001 |
|
| 1002 |
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-5 space-y-5">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
{/* Age */}
|
| 1004 |
<div>
|
| 1005 |
<label className="flex items-baseline justify-between text-xs mb-1.5">
|
|
|
|
| 925 |
onClose: () => void;
|
| 926 |
uiLang: UILang;
|
| 927 |
}) {
|
| 928 |
+
// KI-077 — pre-fill from initialProfile (the chat-captured state). If the
|
| 929 |
+
// chat already heard "I am Rohit Sar, 29, just me, Mumbai", every chip
|
| 930 |
+
// below renders with those values selected when the panel opens.
|
| 931 |
+
const [name, setName] = useState<string>(initialProfile.name ?? "");
|
| 932 |
const [age, setAge] = useState<number | null>(initialProfile.age ?? null);
|
| 933 |
const [dependents, setDependents] = useState<string>(initialProfile.dependents ?? "self");
|
| 934 |
const [budget, setBudget] = useState<string>(initialProfile.budget_band ?? "");
|
|
|
|
| 941 |
const [parentsAgeMax, setParentsAgeMax] = useState<number | null>(initialProfile.parents_age_max ?? null);
|
| 942 |
const [busy, setBusy] = useState(false);
|
| 943 |
|
| 944 |
+
// KI-077 — keep panel in sync if the chat captures new fields while the
|
| 945 |
+
// panel is open. Otherwise the user sees stale state.
|
| 946 |
+
useEffect(() => {
|
| 947 |
+
if (initialProfile.name && !name) setName(initialProfile.name);
|
| 948 |
+
if (initialProfile.age != null && age == null) setAge(initialProfile.age);
|
| 949 |
+
if (initialProfile.dependents && dependents === "self") setDependents(initialProfile.dependents);
|
| 950 |
+
if (initialProfile.budget_band && !budget) setBudget(initialProfile.budget_band);
|
| 951 |
+
if (initialProfile.income_band && !income) setIncome(initialProfile.income_band);
|
| 952 |
+
if (initialProfile.location_tier && !city) setCity(initialProfile.location_tier);
|
| 953 |
+
if (initialProfile.health_conditions?.length && !conditions.length) setConditions(initialProfile.health_conditions);
|
| 954 |
+
if (initialProfile.existing_cover_inr != null && existingCover == null) setExistingCover(initialProfile.existing_cover_inr);
|
| 955 |
+
if (initialProfile.primary_goal && !primaryGoal) setPrimaryGoal(initialProfile.primary_goal);
|
| 956 |
+
if (initialProfile.parents_age_max != null && parentsAgeMax == null) setParentsAgeMax(initialProfile.parents_age_max);
|
| 957 |
+
if (initialProfile.parents_has_ped != null && parentsHasPed == null) setParentsHasPed(initialProfile.parents_has_ped);
|
| 958 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 959 |
+
}, [initialProfile]);
|
| 960 |
+
|
| 961 |
const hindi = uiLang === "hi";
|
| 962 |
|
| 963 |
const toggleCondition = (c: string) => {
|
|
|
|
| 976 |
try {
|
| 977 |
const resp = await postProfileUpdate({
|
| 978 |
session_id: sid,
|
| 979 |
+
name: name.trim() || undefined, // KI-077 — submit the name too
|
| 980 |
age: age ?? undefined,
|
| 981 |
dependents: dependents || undefined,
|
| 982 |
budget_band: budget || undefined,
|
|
|
|
| 1022 |
</div>
|
| 1023 |
|
| 1024 |
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-5 space-y-5">
|
| 1025 |
+
{/* KI-077 — Name (free text). Captured from chat if user introduced
|
| 1026 |
+
themselves; saved to the named-profile store so returning visits
|
| 1027 |
+
auto-load. */}
|
| 1028 |
+
<div>
|
| 1029 |
+
<label className="flex items-baseline justify-between text-xs mb-1.5">
|
| 1030 |
+
<span className="font-semibold">{hindi ? "आपका नाम" : "Your name"}</span>
|
| 1031 |
+
{initialProfile.name && (
|
| 1032 |
+
<span className="text-[10px] text-[var(--primary)]">
|
| 1033 |
+
{hindi ? "chat से लिया गया" : "captured from chat"}
|
| 1034 |
+
</span>
|
| 1035 |
+
)}
|
| 1036 |
+
</label>
|
| 1037 |
+
<input
|
| 1038 |
+
type="text"
|
| 1039 |
+
value={name}
|
| 1040 |
+
onChange={(e) => setName(e.target.value)}
|
| 1041 |
+
placeholder={hindi ? "जैसे, रोहित" : "e.g., Rohit Sar"}
|
| 1042 |
+
maxLength={50}
|
| 1043 |
+
className="w-full text-sm px-3 py-1.5 rounded-md border border-[var(--border)] bg-[var(--card)] focus:outline-none focus:border-[var(--primary)]"
|
| 1044 |
+
/>
|
| 1045 |
+
<p className="text-[10px] text-[var(--muted-foreground)] mt-0.5">
|
| 1046 |
+
{hindi
|
| 1047 |
+
? "अगली बार आने पर मैं आपकी profile पहचान लूंगा।"
|
| 1048 |
+
: "I'll recognise you on your next visit so you don't repeat this."}
|
| 1049 |
+
</p>
|
| 1050 |
+
</div>
|
| 1051 |
+
|
| 1052 |
{/* Age */}
|
| 1053 |
<div>
|
| 1054 |
<label className="flex items-baseline justify-between text-xs mb-1.5">
|
|
@@ -333,6 +333,7 @@ export async function getMarketplace(session_id?: string): Promise<MarketplaceRe
|
|
| 333 |
}
|
| 334 |
|
| 335 |
export type UserProfile = {
|
|
|
|
| 336 |
age?: number | null;
|
| 337 |
dependents?: string | null;
|
| 338 |
income_band?: string | null;
|
|
|
|
| 333 |
}
|
| 334 |
|
| 335 |
export type UserProfile = {
|
| 336 |
+
name?: string | null; // KI-077 — captured from chat or entered in profile panel
|
| 337 |
age?: number | null;
|
| 338 |
dependents?: string | null;
|
| 339 |
income_band?: string | null;
|