Spaces:
Sleeping
Richer portrait + collapsible mirror feed
Browse filesPersonalitySynthesizer:
- 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>
|
@@ -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 =
|
| 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:
|
|
@@ -64,16 +64,14 @@ class PersonalitySynthesizer:
|
|
| 64 |
|
| 65 |
session_log = ""
|
| 66 |
if sessions_data:
|
| 67 |
-
session_log = "\nSESSION LOG (
|
| 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
|
| 74 |
session_log += (
|
| 75 |
-
f" {i}. {s['context']} | {
|
| 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
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
@@ -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
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 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 |
}
|