rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
2bb3898
·
1 Parent(s): 5fc01a7

feat(profile): KI-077 — Name field + show current captured profile in Build-your-profile panel

Browse files

User 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 CHANGED
@@ -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,
frontend/src/app/page.tsx CHANGED
@@ -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">
frontend/src/lib/api.ts CHANGED
@@ -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;