Harsh200415 Claude Sonnet 4.6 commited on
Commit
6744cd1
·
1 Parent(s): 19b7615

Richer portrait + collapsible mirror feed

Browse files

PersonalitySynthesizer:
- Paragraph rules now direct the LLM across 7 behavioral vocabulary dimensions:
thinking style, argument framing, conversation navigation, handling challenge,
clarity/precision, listening quality, warmth/relational style
- Session log now passes fingerprint[:350] (was [:200]) as the PRIMARY source
rather than raw dimension scores — the qualitative text matters more
- Explicit rule: do NOT make dominance/talk ratio the portrait centrepiece
if fingerprints reveal more interesting things
- Bump _SYNTHESIS_VERSION 5 → 6 to force portrait regeneration

ProfileView:
- MirrorFeed is now collapsible — shows 3 items by default
- "Show N more" button expands to full list
- "Show less" collapses back to 3
- Extracted MirrorFeedItem component for cleaner rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

backend/main.py CHANGED
@@ -287,7 +287,7 @@ def _build_sessions_data(parsed: list, dim_paths: dict) -> list:
287
  def _get_or_synthesize_personality(user_id: str, session_count: int,
288
  profile_data: dict, dim_averages: dict,
289
  sessions_data: list = None) -> dict:
290
- _SYNTHESIS_VERSION = 5 # bump when prompt changes to invalidate old cache
291
  cache_key = f"{user_id}:{session_count}:v{_SYNTHESIS_VERSION}"
292
 
293
  if cache_key in _personality_cache:
 
287
  def _get_or_synthesize_personality(user_id: str, session_count: int,
288
  profile_data: dict, dim_averages: dict,
289
  sessions_data: list = None) -> dict:
290
+ _SYNTHESIS_VERSION = 6 # bump when prompt changes to invalidate old cache
291
  cache_key = f"{user_id}:{session_count}:v{_SYNTHESIS_VERSION}"
292
 
293
  if cache_key in _personality_cache:
backend/pipeline/personality_synthesizer.py CHANGED
@@ -64,16 +64,14 @@ class PersonalitySynthesizer:
64
 
65
  session_log = ""
66
  if sessions_data:
67
- session_log = "\nSESSION LOG (reference by context type when writing the shape narrative):\n"
68
  for i, s in enumerate(sessions_data[-10:], 1):
69
- scores = ", ".join(f"{k}={v}" for k, v in s.get("dim_scores", {}).items())
70
  notable = s.get("notable_pattern", "")
71
  notable_str = f' | pattern: "{notable}"' if notable else ""
72
  fingerprint = s.get("fingerprint", "")
73
- fp_str = f'\n behavioral summary: {fingerprint[:200]}…' if fingerprint else ""
74
  session_log += (
75
- f" {i}. {s['context']} | {scores} "
76
- f"| fillers={s['filler_rate']}/100w | talk={s['talk_ratio_pct']}%"
77
  f"{notable_str}{fp_str}\n"
78
  )
79
 
@@ -85,13 +83,21 @@ class PersonalitySynthesizer:
85
  TASK: Write a personality portrait paragraph, 5 dimension scores with labels, and one shape narrative.
86
 
87
  PARAGRAPH RULES:
88
- 1. 3–4 sentences maximum. Write as if describing this person to someone who knows them.
89
- 2. NO raw numbers or percentages. No "57% of the time" or "211 WPM".
90
- Only use numbers if they describe a striking contrast — and even then, phrase it qualitatively ("nearly double", "significantly slower").
91
- 3. Focus on behavioural character: how they show up, what their tendencies are, what makes them distinct.
92
- 4. Reference context contrasts only where the data actually supports them.
93
- 5. Use "you" / "your" — address the person directly.
94
- 6. Never write "seems to" or "appears to" — be direct about observed patterns.
 
 
 
 
 
 
 
 
95
 
96
  SHAPE NARRATIVE RULES (one paragraph shown below all 5 scores):
97
  1. 3–4 sentences. Synthesize what the COMBINATION of all 5 scores reveals — not a summary of each one individually.
 
64
 
65
  session_log = ""
66
  if sessions_data:
67
+ session_log = "\nSESSION LOG (the behavioral fingerprints are your PRIMARY source — use them, not just the numbers):\n"
68
  for i, s in enumerate(sessions_data[-10:], 1):
 
69
  notable = s.get("notable_pattern", "")
70
  notable_str = f' | pattern: "{notable}"' if notable else ""
71
  fingerprint = s.get("fingerprint", "")
72
+ fp_str = f'\n [{s["context"]}] {fingerprint[:350]}' if fingerprint else ""
73
  session_log += (
74
+ f" {i}. {s['context']} | talk={s['talk_ratio_pct']}%"
 
75
  f"{notable_str}{fp_str}\n"
76
  )
77
 
 
83
  TASK: Write a personality portrait paragraph, 5 dimension scores with labels, and one shape narrative.
84
 
85
  PARAGRAPH RULES:
86
+ 1. 3–4 sentences maximum. Write as if describing this person to someone who knows them well.
87
+ 2. NO raw numbers or percentages — phrase everything qualitatively.
88
+ 3. VOCABULARY: The portrait must go beyond talk dynamics. Draw from what the fingerprints actually reveal across these dimensions:
89
+ — THINKING STYLE: Do they reframe questions before answering? Take contrarian positions? Think out loud or present polished thoughts? Big-picture or detail-driven?
90
+ — ARGUMENT STYLE: Do they lead with logic, stories, analogies, personal experience, or principles? How do they structure a point?
91
+ — CONVERSATION NAVIGATION: Do they drive topics, shift tone, redirect, or follow? Who owns the direction of their conversations?
92
+ — HANDLING CHALLENGE: When pushed back on, do they get curious, defensive, or adaptive? Do they change position? What moves them?
93
+ — CLARITY & PRECISION: Do they commit to positions or hedge constantly? Do they use concrete examples or stay abstract?
94
+ — LISTENING & RESPONSE: Do they build on what others say, or pivot immediately to their own point? Do they ask probing questions?
95
+ — WARMTH & RELATIONAL STYLE: Humor, validation, emotional calibration — do they connect or transact?
96
+ 4. If talk ratio or dominance is already the most obvious thing, do NOT make it the centrepiece unless it is truly the most defining characteristic. If the fingerprints reveal more interesting things about thinking style, framing, or perspective — surface those instead.
97
+ 5. The portrait should make the person feel SEEN as an individual — not like a generic assessment report.
98
+ 6. Reference context contrasts only where the data actually supports them.
99
+ 7. Use "you" / "your" — address the person directly.
100
+ 8. Never write "seems to" or "appears to" — be direct about observed patterns.
101
 
102
  SHAPE NARRATIVE RULES (one paragraph shown below all 5 scores):
103
  1. 3–4 sentences. Synthesize what the COMBINATION of all 5 scores reveals — not a summary of each one individually.
frontend/src/components/ProfileView.jsx CHANGED
@@ -94,7 +94,52 @@ const FEED_TYPE_CONFIG = {
94
  pattern: { color: "#f59e0b", label: "Consistent pattern", icon: "●" },
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  function MirrorFeed({ insights }) {
 
 
98
  if (!insights) return null
99
 
100
  if (!insights.length) {
@@ -106,50 +151,39 @@ function MirrorFeed({ insights }) {
106
  )
107
  }
108
 
 
 
 
 
109
  return (
110
- <div style={{ background: "#151922", border: "1px solid #1e2438",
111
- borderRadius: 12, overflow: "hidden",
112
- boxShadow: "0 2px 16px rgba(0,0,0,0.3)" }}>
113
- {insights.map((insight, i) => {
114
- const cfg = FEED_TYPE_CONFIG[insight.type] || FEED_TYPE_CONFIG.pattern
115
- return (
116
- <RevealItem key={i} index={i}>
117
- <div style={{
118
- display: "flex", gap: 14, padding: "14px 18px",
119
- borderBottom: i < insights.length - 1 ? "1px solid #131827" : "none",
120
- alignItems: "flex-start",
121
- }}>
122
- <div style={{
123
- width: 32, height: 32, borderRadius: 8, flexShrink: 0,
124
- background: `${cfg.color}18`,
125
- border: `1px solid ${cfg.color}30`,
126
- display: "flex", alignItems: "center", justifyContent: "center",
127
- fontSize: 14, color: cfg.color, fontWeight: 700, marginTop: 1,
128
- }}>
129
- {cfg.icon}
130
- </div>
131
- <div style={{ flex: 1, minWidth: 0 }}>
132
- <div style={{ fontSize: 11, fontWeight: 700, color: cfg.color,
133
- textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 5 }}>
134
- {cfg.label}
135
- </div>
136
- <p style={{ margin: 0, fontSize: 13, color: "#c4c2d8", lineHeight: 1.75 }}>
137
- {insight.text}
138
- </p>
139
- {insight.tip && (
140
- <p style={{
141
- margin: "10px 0 0", fontSize: 12, color: "#6b6888", lineHeight: 1.65,
142
- paddingTop: 10, borderTop: "1px solid #131827",
143
- }}>
144
- <span style={{ color: "#4a4865", fontWeight: 600 }}>— </span>
145
- {insight.tip}
146
- </p>
147
- )}
148
- </div>
149
- </div>
150
- </RevealItem>
151
- )
152
- })}
153
  </div>
154
  )
155
  }
 
94
  pattern: { color: "#f59e0b", label: "Consistent pattern", icon: "●" },
95
  }
96
 
97
+ const FEED_MIN_VISIBLE = 3
98
+
99
+ function MirrorFeedItem({ insight, i, total }) {
100
+ const cfg = FEED_TYPE_CONFIG[insight.type] || FEED_TYPE_CONFIG.pattern
101
+ return (
102
+ <RevealItem index={i}>
103
+ <div style={{
104
+ display: "flex", gap: 14, padding: "14px 18px",
105
+ borderBottom: i < total - 1 ? "1px solid #131827" : "none",
106
+ alignItems: "flex-start",
107
+ }}>
108
+ <div style={{
109
+ width: 32, height: 32, borderRadius: 8, flexShrink: 0,
110
+ background: `${cfg.color}18`,
111
+ border: `1px solid ${cfg.color}30`,
112
+ display: "flex", alignItems: "center", justifyContent: "center",
113
+ fontSize: 14, color: cfg.color, fontWeight: 700, marginTop: 1,
114
+ }}>
115
+ {cfg.icon}
116
+ </div>
117
+ <div style={{ flex: 1, minWidth: 0 }}>
118
+ <div style={{ fontSize: 11, fontWeight: 700, color: cfg.color,
119
+ textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 5 }}>
120
+ {cfg.label}
121
+ </div>
122
+ <p style={{ margin: 0, fontSize: 13, color: "#c4c2d8", lineHeight: 1.75 }}>
123
+ {insight.text}
124
+ </p>
125
+ {insight.tip && (
126
+ <p style={{
127
+ margin: "10px 0 0", fontSize: 12, color: "#6b6888", lineHeight: 1.65,
128
+ paddingTop: 10, borderTop: "1px solid #131827",
129
+ }}>
130
+ <span style={{ color: "#4a4865", fontWeight: 600 }}>— </span>
131
+ {insight.tip}
132
+ </p>
133
+ )}
134
+ </div>
135
+ </div>
136
+ </RevealItem>
137
+ )
138
+ }
139
+
140
  function MirrorFeed({ insights }) {
141
+ const [expanded, setExpanded] = useState(false)
142
+
143
  if (!insights) return null
144
 
145
  if (!insights.length) {
 
151
  )
152
  }
153
 
154
+ const hasMore = insights.length > FEED_MIN_VISIBLE
155
+ const visible = expanded ? insights : insights.slice(0, FEED_MIN_VISIBLE)
156
+ const hiddenCount = insights.length - FEED_MIN_VISIBLE
157
+
158
  return (
159
+ <div>
160
+ <div style={{ background: "#151922", border: "1px solid #1e2438",
161
+ borderRadius: 12, overflow: "hidden",
162
+ boxShadow: "0 2px 16px rgba(0,0,0,0.3)" }}>
163
+ {visible.map((insight, i) => (
164
+ <MirrorFeedItem key={insight.signal || i} insight={insight} i={i} total={visible.length} />
165
+ ))}
166
+ </div>
167
+
168
+ {hasMore && (
169
+ <button
170
+ onClick={() => setExpanded(v => !v)}
171
+ style={{
172
+ marginTop: 10, width: "100%", padding: "9px 0",
173
+ background: "none", border: "1px solid #1e2438",
174
+ borderRadius: 8, cursor: "pointer", fontSize: 12,
175
+ color: "#4a4865", transition: "all 0.15s",
176
+ display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
177
+ }}
178
+ onMouseEnter={e => { e.currentTarget.style.borderColor = "#2e3450"; e.currentTarget.style.color = "#8b89aa" }}
179
+ onMouseLeave={e => { e.currentTarget.style.borderColor = "#1e2438"; e.currentTarget.style.color = "#4a4865" }}
180
+ >
181
+ <span style={{ display: "inline-block",
182
+ transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
183
+ transition: "transform 0.2s", fontSize: 10 }}>▼</span>
184
+ {expanded ? "Show less" : `Show ${hiddenCount} more`}
185
+ </button>
186
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  </div>
188
  )
189
  }