Mirror-Backend / frontend /src /components /ProfileView.jsx
Harsh200415's picture
LLM-generated portrait tags (max 4, behavior-specific)
2e7ca87
Raw
History Blame Contribute Delete
32.1 kB
import { useState, useEffect } from "react"
import {
RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis,
LineChart, Line, XAxis, YAxis, Tooltip,
ResponsiveContainer, CartesianGrid,
} from "recharts"
import api from "../lib/api"
import Reveal, { RevealItem } from "./Reveal"
const G = "linear-gradient(135deg, #1d4ed8 0%, #0891b2 100%)"
const CONTEXT_LABELS = {
social: "Casual & Low-Stakes", collaborative: "Collaborative",
evaluative: "Interview & Review · High Stakes", influential: "Persuading & Pitching",
negotiation: "Negotiation", adversarial: "Conflict & Friction",
developmental: "Coaching & Feedback", support: "Supportive Listening",
intimate: "Deep Personal", casual: "Casual & Low-Stakes", meeting: "Meeting",
job_interview: "Interview & Review · High Stakes", disagreement: "Conflict & Friction",
presentation: "Interview & Review · High Stakes", sales_call: "Persuading & Pitching",
feedback_conversation: "Coaching & Feedback", coaching_call: "Coaching & Feedback",
first_date: "Deep Personal",
}
const SIGNAL_OPTIONS = [
{ key: "talk_ratio", label: "Talk Ratio", unit: "%" },
{ key: "wpm", label: "Speech Rate", unit: "wpm" },
{ key: "filler_rate", label: "Filler Rate", unit: "/100w" },
{ key: "interruptions_given", label: "Interruptions", unit: "x" },
{ key: "silence_ratio", label: "Silence", unit: "%" },
{ key: "response_latency", label: "Response Latency", unit: "s" },
]
const TALK_RATIO_NORMS = {
evaluative: [55, 80], collaborative: [30, 55], social: [35, 65],
influential: [48, 68], negotiation: [35, 55], adversarial: [35, 55],
developmental: [25, 45], support: [15, 40], intimate: [35, 60],
}
function useCountUp(target, duration = 900) {
const [value, setValue] = useState(0)
useEffect(() => {
if (!target) return
const start = Date.now()
const tick = () => {
const elapsed = Date.now() - start
const progress = Math.min(elapsed / duration, 1)
const eased = 1 - Math.pow(1 - progress, 3)
setValue(Math.round(eased * target))
if (progress < 1) requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
}, [target])
return value
}
// ── Section label ─────────────────────────────────────────────────
function SectionLabel({ children }) {
return (
<div style={{
fontSize: 10, fontWeight: 700, letterSpacing: 1.4,
textTransform: "uppercase", marginBottom: 4,
background: G, WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent", backgroundClip: "text",
}}>
{children}
</div>
)
}
// ── Mirror Feed ───────────────────────────────────────────────────
const FEED_TYPE_CONFIG = {
context_contrast: { color: "#818cf8", label: "Context contrast", icon: "↕" },
trend_up: { color: "#34d399", label: "Improving", icon: "↑" },
trend_down: { color: "#fb923c", label: "Declining", icon: "↓" },
pattern: { color: "#f59e0b", label: "Consistent pattern", icon: "●" },
}
const FEED_MIN_VISIBLE = 3
function MirrorFeedItem({ insight, i, total }) {
const cfg = FEED_TYPE_CONFIG[insight.type] || FEED_TYPE_CONFIG.pattern
return (
<RevealItem index={i}>
<div style={{
display: "flex", gap: 14, padding: "14px 18px",
borderBottom: i < total - 1 ? "1px solid #131827" : "none",
alignItems: "flex-start",
}}>
<div style={{
width: 32, height: 32, borderRadius: 8, flexShrink: 0,
background: `${cfg.color}18`,
border: `1px solid ${cfg.color}30`,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 14, color: cfg.color, fontWeight: 700, marginTop: 1,
}}>
{cfg.icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 11, fontWeight: 700, color: cfg.color,
textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 5 }}>
{cfg.label}
</div>
<p style={{ margin: 0, fontSize: 13, color: "#c4c2d8", lineHeight: 1.75 }}>
{insight.text}
</p>
{insight.tip && (
<p style={{
margin: "10px 0 0", fontSize: 12, color: "#6b6888", lineHeight: 1.65,
paddingTop: 10, borderTop: "1px solid #131827",
}}>
<span style={{ color: "#4a4865", fontWeight: 600 }}></span>
{insight.tip}
</p>
)}
</div>
</div>
</RevealItem>
)
}
function MirrorFeed({ insights }) {
const [expanded, setExpanded] = useState(false)
if (!insights) return null
if (!insights.length) {
return (
<p style={{ fontSize: 13, color: "#4a4d6a", margin: 0, lineHeight: 1.7 }}>
Upload conversations across different contexts — patterns that span multiple
sessions will appear here.
</p>
)
}
const hasMore = insights.length > FEED_MIN_VISIBLE
const visible = expanded ? insights : insights.slice(0, FEED_MIN_VISIBLE)
return (
<div>
<div style={{ position: "relative" }}>
<div style={{ background: "#151922", border: "1px solid #1e2438",
borderRadius: 12, overflow: "hidden",
boxShadow: "0 2px 16px rgba(0,0,0,0.3)" }}>
{visible.map((insight, i) => (
<MirrorFeedItem key={insight.signal || i} insight={insight} i={i} total={visible.length} />
))}
</div>
{/* Gradient fade over last item with Show more pill */}
{hasMore && !expanded && (
<div
onClick={() => setExpanded(true)}
style={{
position: "absolute", bottom: 0, left: 0, right: 0,
height: 90,
background: "linear-gradient(to bottom, transparent, #0f1117)",
borderRadius: "0 0 12px 12px",
display: "flex", alignItems: "flex-end",
justifyContent: "center", paddingBottom: 14,
cursor: "pointer",
}}
>
<span style={{
fontSize: 12, fontWeight: 600, color: "#5b9cf6",
background: "rgba(15,17,23,0.85)",
border: "1px solid rgba(29,78,216,0.3)",
borderRadius: 20, padding: "5px 18px",
}}>
Show more
</span>
</div>
)}
</div>
{/* Show less — simple link below when expanded */}
{expanded && hasMore && (
<button
onClick={() => setExpanded(false)}
style={{
marginTop: 8, background: "none", border: "none",
cursor: "pointer", fontSize: 12, color: "#4a4865",
display: "block", width: "100%", textAlign: "center", padding: "6px 0",
}}
>
Show less
</button>
)}
</div>
)
}
// ── Flat dimension bar ────────────────────────────────────────────
function FlatBar({ d }) {
const score = useCountUp(d.score)
return (
<div style={{ display: "flex", alignItems: "center", gap: 14,
padding: "10px 0", borderBottom: "1px solid #131827" }}>
<span style={{ fontSize: 13, fontWeight: 500, color: "#c4c2d8",
minWidth: 130, flexShrink: 0 }}>{d.name}</span>
<div style={{ flex: 1, height: 4, background: "#1e2438", borderRadius: 2 }}>
<div style={{ height: "100%", borderRadius: 2, width: `${score}%`,
background: G, transition: "width 0.5s ease",
boxShadow: "0 0 8px rgba(59,130,246,0.4)" }} />
</div>
<span style={{ fontSize: 14, fontWeight: 700, minWidth: 30, textAlign: "right",
background: G, WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent", backgroundClip: "text" }}>
{score}
</span>
</div>
)
}
// ── Pentagon radar chart ──────────────────────────────────────────
function PentagonChart({ dimensions }) {
if (!dimensions?.length) return null
const SHORT = {
"Listening Quality": "Listening",
"Communication Clarity": "Clarity",
}
const radarData = dimensions.map(d => ({
subject: SHORT[d.name] || d.name,
score: d.score,
fullMark: 100,
}))
return (
<div style={{
background: "#151922", border: "1px solid #1e2438",
borderRadius: 12, padding: "8px 0",
boxShadow: "0 2px 16px rgba(0,0,0,0.3)"
}}>
<ResponsiveContainer width="100%" height={220}>
<RadarChart data={radarData} cx="50%" cy="50%" outerRadius="66%">
<PolarGrid stroke="#1e2438" />
<PolarAngleAxis dataKey="subject" tick={{ fill: "#8b89aa", fontSize: 11 }} />
<PolarRadiusAxis domain={[0, 100]} tick={false} axisLine={false} />
<Radar dataKey="score"
stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.18} strokeWidth={2.5} />
</RadarChart>
</ResponsiveContainer>
</div>
)
}
// ── You Across Contexts ───────────────────────────────────────────
function ContextComparison({ byContext }) {
const contexts = Object.keys(byContext || {})
const [activeCtx, setActiveCtx] = useState(contexts[0] || null)
if (!contexts.length || !activeCtx) return null
const data = byContext[activeCtx]
const norm = TALK_RATIO_NORMS[activeCtx]
const withinNorm = norm ? (data.talk_ratio >= norm[0] && data.talk_ratio <= norm[1]) : null
const normColor = withinNorm === null ? "#8b89aa" : withinNorm ? "#34d399" : "#f59e0b"
return (
<div>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 14 }}>
{contexts.map(ctx => (
<button key={ctx} onClick={() => setActiveCtx(ctx)}
style={{ padding: "5px 12px", borderRadius: 20, fontSize: 12,
cursor: "pointer", border: "1px solid", transition: "all 0.15s",
background: activeCtx === ctx ? "rgba(59,130,246,0.12)" : "#151922",
color: activeCtx === ctx ? "#60a5fa" : "#8b89aa",
borderColor: activeCtx === ctx ? "rgba(59,130,246,0.3)" : "#1e2438" }}>
{CONTEXT_LABELS[ctx] || ctx}
<span style={{ opacity: 0.5, marginLeft: 5, fontSize: 10 }}>
{byContext[ctx].count}×
</span>
</button>
))}
</div>
<div style={{ background: "#151922", border: "1px solid #1e2438",
borderRadius: 10, padding: "16px 18px" }}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 16 }}>
<div>
<div style={{ fontSize: 11, color: "#4a4865", marginBottom: 5 }}>Talk ratio</div>
<div style={{ fontSize: 22, fontWeight: 700, color: "#f0eeff", lineHeight: 1 }}>
{data.talk_ratio}<span style={{ fontSize: 13, color: "#8b89aa" }}>%</span>
</div>
{norm && (
<div style={{ fontSize: 11, color: normColor, marginTop: 4 }}>
{withinNorm ? "✓" : "↑"} norm {norm[0]}–{norm[1]}%
</div>
)}
</div>
<div>
<div style={{ fontSize: 11, color: "#4a4865", marginBottom: 5 }}>Speech rate</div>
<div style={{ fontSize: 22, fontWeight: 700, color: "#f0eeff", lineHeight: 1 }}>
{data.wpm}<span style={{ fontSize: 13, color: "#8b89aa" }}> wpm</span>
</div>
</div>
<div>
<div style={{ fontSize: 11, color: "#4a4865", marginBottom: 5 }}>Filler rate</div>
<div style={{ fontSize: 22, fontWeight: 700, color: "#f0eeff", lineHeight: 1 }}>
{data.filler_rate}<span style={{ fontSize: 13, color: "#8b89aa" }}>/100w</span>
</div>
</div>
</div>
</div>
</div>
)
}
// ── What's Changed ────────────────────────────────────────────────
function WhatsChanged({ trendLines, lastDelta }) {
const dimChanges = lastDelta?.changes || []
const signalTrends = trendLines || []
if (!dimChanges.length && !signalTrends.length) return null
return (
<div>
<div style={{ marginBottom: 12 }}>
<SectionLabel>Trends</SectionLabel>
<h2 style={{ fontSize: 14, fontWeight: 700, margin: 0, color: "#f0eeff" }}>
What's Changed
</h2>
<p style={{ fontSize: 12, color: "#4a4d6a", margin: "4px 0 0" }}>
Movement detected across your sessions
</p>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
{dimChanges.map((c, i) => (
<div key={i} style={{
display: "flex", alignItems: "center", gap: 5,
padding: "7px 13px", borderRadius: 20, fontSize: 12, fontWeight: 600,
background: c.direction === "up" ? "rgba(52,211,153,0.08)" : "rgba(248,113,113,0.08)",
border: `1px solid ${c.direction === "up" ? "rgba(52,211,153,0.25)" : "rgba(248,113,113,0.25)"}`,
color: c.direction === "up" ? "#34d399" : "#f87171",
}}>
{c.direction === "up" ? "↑" : "↓"} {c.dimension}
<span style={{ opacity: 0.7 }}>
{" "}{c.direction === "up" ? "+" : ""}{c.diff}pts
</span>
</div>
))}
{signalTrends.map((t, i) => (
<div key={`t${i}`} style={{
display: "flex", alignItems: "center", gap: 5,
padding: "7px 13px", borderRadius: 20, fontSize: 12, fontWeight: 600,
background: t.direction === "improved" ? "rgba(52,211,153,0.08)" : "rgba(248,113,113,0.08)",
border: `1px solid ${t.direction === "improved" ? "rgba(52,211,153,0.25)" : "rgba(248,113,113,0.25)"}`,
color: t.direction === "improved" ? "#34d399" : "#f87171",
}}>
{t.direction === "improved" ? "↗" : "↘"}
{" "}{t.signal.replace(/_/g, " ")}
<span style={{ opacity: 0.7 }}>
{" "}{t.old}{t.unit} → {t.new}{t.unit}
</span>
</div>
))}
</div>
</div>
)
}
// ── Signal Trends (collapsible) ───────────────────────────────────
function SignalTrends({ chartData, trendLines }) {
const [open, setOpen] = useState(false)
const [activeSignal, setActiveSignal] = useState("talk_ratio")
if (chartData.length < 2) return null
const signalConfig = SIGNAL_OPTIONS.find(s => s.key === activeSignal)
return (
<div className="card" style={{ border: "1px solid #1e2438", borderRadius: 12,
background: "#151922", overflow: "hidden" }}>
<div onClick={() => setOpen(v => !v)}
style={{ padding: "14px 18px", display: "flex", justifyContent: "space-between",
alignItems: "center", cursor: "pointer",
borderBottom: open ? "1px solid #1e2438" : "none" }}>
<span style={{ fontSize: 13, fontWeight: 700, color: "#f0eeff" }}>Signal Trends</span>
<span style={{ fontSize: 11, color: "#4a4865" }}>{open ? "▲ close" : "▼ expand"}</span>
</div>
{open && (
<div style={{ padding: "16px 18px" }}>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 14 }}>
{SIGNAL_OPTIONS.map(s => {
const isActive = activeSignal === s.key
return (
<button key={s.key} onClick={() => setActiveSignal(s.key)}
style={{ padding: "5px 12px", borderRadius: 20, fontSize: 12,
cursor: "pointer", fontWeight: isActive ? 600 : 400,
background: isActive ? "rgba(59,130,246,0.12)" : "#0e1320",
color: isActive ? "#60a5fa" : "#8b89aa",
border: isActive ? "1px solid rgba(59,130,246,0.3)" : "1px solid #1e2438",
transition: "all 0.15s" }}>
{s.label}
</button>
)
})}
</div>
{trendLines?.filter(t => t.signal === activeSignal).map((t, i) => (
<div key={i} style={{
display: "inline-flex", alignItems: "center", gap: 6,
padding: "4px 12px", borderRadius: 20, fontSize: 12, marginBottom: 10,
background: t.direction === "improved" ? "rgba(52,211,153,0.08)" : "rgba(248,113,113,0.08)",
border: `1px solid ${t.direction === "improved" ? "rgba(52,211,153,0.25)" : "rgba(248,113,113,0.25)"}`,
color: t.direction === "improved" ? "#34d399" : "#f87171",
}}>
{t.direction === "improved" ? "↗" : "↘"}
{" "}{signalConfig?.label} {t.direction}: {t.old}{signalConfig?.unit} → {t.new}{signalConfig?.unit}
</div>
))}
<div style={{ borderRadius: 8, padding: "12px 0" }}>
<ResponsiveContainer width="100%" height={180}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e2438" />
<XAxis dataKey="label" fontSize={11} tick={{ fill: "#8b89aa" }}
axisLine={{ stroke: "#1e2438" }} tickLine={{ stroke: "#1e2438" }} />
<YAxis fontSize={11} tick={{ fill: "#8b89aa" }} width={38}
axisLine={{ stroke: "#1e2438" }} tickLine={{ stroke: "#1e2438" }}
tickFormatter={v => `${v}${signalConfig?.unit || ""}`} />
<Tooltip
contentStyle={{ background: "#151922", border: "1px solid #1e2438", borderRadius: 8 }}
labelStyle={{ color: "#8b89aa" }} itemStyle={{ color: "#f0eeff" }}
formatter={v => [`${v}${signalConfig?.unit || ""}`, signalConfig?.label]}
labelFormatter={(_, p) => p?.[0]?.payload?.date || ""}
/>
<Line type="monotone" dataKey={activeSignal}
stroke="#3b82f6" strokeWidth={2.5}
dot={{ r: 4, fill: "#3b82f6", strokeWidth: 0 }}
activeDot={{ r: 6, fill: "#22d3ee" }} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
)}
</div>
)
}
// ── Main ProfileView ──────────────────────────────────────────────
export default function ProfileView({ active, onUpload }) {
const [profile, setProfile] = useState(null)
const [trends, setTrends] = useState([])
const [loading, setLoading] = useState(true)
const [blindSpotsOpen, setBlindSpotsOpen] = useState(false)
useEffect(() => {
if (!active) return
setLoading(true)
Promise.all([api.get("/api/profile"), api.get("/api/trends")])
.then(([profileRes, trendsRes]) => {
setProfile(profileRes.data)
setTrends(trendsRes.data.data || [])
})
.catch(console.error)
.finally(() => setLoading(false))
}, [active])
if (loading) return (
<div style={{ textAlign: "center", padding: 60, color: "#4a4865" }}>
Loading your profile…
</div>
)
if (!profile || profile.insufficient_data) {
return (
<div style={{ textAlign: "center", padding: "60px 20px" }}>
<div style={{ display: "flex", justifyContent: "center", marginBottom: 16 }}>
<svg width="52" height="52" viewBox="0 0 52 52" fill="none">
<defs>
<linearGradient id="pv-empty-g" x1="0" y1="0" x2="52" y2="0" gradientUnits="userSpaceOnUse">
<stop stopColor="#1d4ed8"/><stop offset="1" stopColor="#0891b2"/>
</linearGradient>
</defs>
<rect x="2" y="20" width="6" height="6" rx="3" fill="url(#pv-empty-g)" opacity=".35"/>
<rect x="11" y="13" width="6" height="13" rx="3" fill="url(#pv-empty-g)" opacity=".6"/>
<rect x="20" y="6" width="8" height="20" rx="4" fill="url(#pv-empty-g)"/>
<rect x="31" y="13" width="6" height="13" rx="3" fill="url(#pv-empty-g)" opacity=".6"/>
<rect x="40" y="20" width="6" height="6" rx="3" fill="url(#pv-empty-g)" opacity=".35"/>
<line x1="0" y1="28" x2="52" y2="28" stroke="#1e2438" strokeWidth="1.25"/>
<rect x="2" y="29" width="6" height="6" rx="3" fill="url(#pv-empty-g)" opacity=".15"/>
<rect x="11" y="29" width="6" height="13" rx="3" fill="url(#pv-empty-g)" opacity=".27"/>
<rect x="20" y="29" width="8" height="20" rx="4" fill="url(#pv-empty-g)" opacity=".33"/>
<rect x="31" y="29" width="6" height="13" rx="3" fill="url(#pv-empty-g)" opacity=".27"/>
<rect x="40" y="29" width="6" height="6" rx="3" fill="url(#pv-empty-g)" opacity=".15"/>
</svg>
</div>
<h2 style={{ fontSize: 20, fontWeight: 700, margin: "0 0 10px", color: "#f0eeff" }}>
Your mirror is waiting
</h2>
<p style={{ fontSize: 14, color: "#8b89aa", margin: "0 0 24px", lineHeight: 1.6 }}>
Upload your first conversation to see your behavioral profile.
</p>
<button onClick={onUpload} className="btn-grad"
style={{ padding: "12px 28px", background: G, color: "white",
border: "none", borderRadius: 8, fontSize: 14, cursor: "pointer",
fontWeight: 600, boxShadow: "0 0 24px rgba(59,130,246,0.3)" }}>
Upload a conversation
</button>
</div>
)
}
const { by_context, trends: trendLines, session_count, personality,
blind_spots, completeness, completeness_label, mirror_feed,
recurring_coaching } = profile
const keywords = (personality?.tags || []).slice(0, 4)
const chartData = trends.map((point, i) => ({
...point,
label: `S${i + 1}`,
date: new Date(point.date).toLocaleDateString("en-IN", { day: "numeric", month: "short" }),
}))
const hasContextData = by_context && Object.keys(by_context).length > 0
return (
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
{/* Header */}
<Reveal>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
<div style={{ flex: 1, minWidth: 0 }}>
<h2 style={{ fontSize: 16, fontWeight: 700, margin: 0, color: "#f0eeff" }}>
Your Behavioral Profile
</h2>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 6 }}>
<span style={{ fontSize: 12, color: "#4a4865" }}>
{session_count} session{session_count > 1 ? "s" : ""}
</span>
{completeness_label && (
<>
<span style={{ color: "#1e2438", fontSize: 12 }}>·</span>
<span style={{ fontSize: 12, color: "#4a4865" }}>{completeness_label}</span>
<div style={{ width: 60, height: 3, background: "#1e2438",
borderRadius: 2, overflow: "hidden" }}>
<div style={{ height: "100%", borderRadius: 2,
width: `${completeness}%`, background: G,
transition: "width 0.6s ease" }} />
</div>
</>
)}
</div>
{blind_spots?.length > 0 && (
<div style={{ marginTop: 10 }}>
<button
onClick={() => setBlindSpotsOpen(v => !v)}
style={{ background: "none", border: "none", padding: 0, cursor: "pointer",
display: "flex", alignItems: "center", gap: 5 }}>
<span style={{ fontSize: 10, color: "#3a3a52", display: "inline-block",
transform: blindSpotsOpen ? "rotate(90deg)" : "rotate(0deg)",
transition: "transform 0.2s" }}></span>
<span style={{ fontSize: 12, color: "#4a4865" }}>
{blind_spots.length} context{blind_spots.length > 1 ? "s" : ""} missing
</span>
</button>
{blindSpotsOpen && (
<div style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 6 }}>
{blind_spots.map((spot, i) => (
<div key={i} style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
<span style={{ fontSize: 11, color: "#3a3a52", marginTop: 1, flexShrink: 0 }}>·</span>
<div>
<span style={{ fontSize: 12, fontWeight: 600, color: "#6b6888" }}>{spot.label}</span>
<span style={{ fontSize: 12, color: "#4a4865" }}> — {spot.message}</span>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
<button onClick={onUpload} className="btn-grad"
style={{ padding: "8px 18px", background: G, color: "white",
border: "none", borderRadius: 7, fontSize: 13, cursor: "pointer",
fontWeight: 600, boxShadow: "0 0 18px rgba(59,130,246,0.25)", flexShrink: 0,
marginLeft: 16 }}>
+ Upload
</button>
</div>
</Reveal>
{/* Your Portrait — comes first, it's the centrepiece */}
{personality && (
<Reveal delay={80}>
<div>
<SectionLabel>Your Portrait</SectionLabel>
<div style={{
borderLeft: "3px solid rgba(59,130,246,0.65)",
paddingLeft: 16, marginTop: 10,
}}>
<p style={{ fontSize: 14, color: "#c4c2d8",
lineHeight: 1.9, margin: 0, textAlign: "justify" }}>
{personality.paragraph}
</p>
{keywords.length > 0 && (
<div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 12 }}>
{keywords.map((kw, i) => (
<span key={i} style={{
fontSize: 12, fontWeight: 600, padding: "5px 14px",
borderRadius: 6, letterSpacing: 0.2,
background: "linear-gradient(#151922, #151922) padding-box, linear-gradient(135deg, rgba(59,130,246,0.5), rgba(34,211,238,0.5)) border-box",
border: "1px solid transparent",
color: "#60a5fa", whiteSpace: "nowrap",
}}>
{kw}
</span>
))}
</div>
)}
</div>
</div>
</Reveal>
)}
{/* Mirror Feed */}
<Reveal delay={80}>
<div>
<div style={{ marginBottom: 12 }}>
<SectionLabel>Cross-Session</SectionLabel>
<h2 style={{ fontSize: 14, fontWeight: 700, margin: 0, color: "#f0eeff" }}>
Mirror Feed
</h2>
</div>
<MirrorFeed insights={mirror_feed} />
</div>
</Reveal>
{/* Recurring Coaching */}
{recurring_coaching?.length > 0 && (
<Reveal delay={100}>
<div>
<div style={{ marginBottom: 12 }}>
<SectionLabel>Keeps Coming Up</SectionLabel>
<h2 style={{ fontSize: 14, fontWeight: 700, margin: 0, color: "#f0eeff" }}>
Recurring Themes
</h2>
<p style={{ fontSize: 12, color: "#4a4d6a", margin: "4px 0 0" }}>
Areas flagged in multiple sessions — these are your persistent development opportunities
</p>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{recurring_coaching.map((item, i) => {
const accent = ["#f87171", "#fb923c", "#818cf8"][i] || "#8b89aa"
return (
<div key={item.area} style={{
padding: "14px 16px", borderRadius: 10,
background: "#151922", border: `1px solid #1e2438`,
borderLeft: `3px solid ${accent}`,
}}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: item.tip ? 8 : 0 }}>
<span style={{ fontSize: 13, fontWeight: 700, color: "#f0eeff",
textTransform: "capitalize" }}>
{item.area}
</span>
<span style={{ fontSize: 11, color: accent, fontWeight: 600,
background: `${accent}15`, border: `1px solid ${accent}30`,
borderRadius: 20, padding: "3px 10px", whiteSpace: "nowrap", marginLeft: 12 }}>
{item.count} of {session_count} sessions
</span>
</div>
{item.tip && (
<p style={{ margin: 0, fontSize: 12, color: "#8b89aa", lineHeight: 1.65 }}>
{item.tip}
</p>
)}
</div>
)
})}
</div>
</div>
</Reveal>
)}
{/* You Across Contexts */}
{hasContextData && (
<Reveal>
<div>
<div style={{ marginBottom: 16 }}>
<SectionLabel>Context Breakdown</SectionLabel>
<h2 style={{ fontSize: 14, fontWeight: 700, margin: 0, color: "#f0eeff" }}>
You Across Contexts
</h2>
<p style={{ fontSize: 12, color: "#4a4d6a", margin: "4px 0 0" }}>
How your patterns shift depending on the room
</p>
</div>
<ContextComparison byContext={by_context} />
</div>
</Reveal>
)}
{/* Your Shape — pentagon + bars */}
{personality?.dimensions?.length > 0 && (
<Reveal>
<div>
<div style={{ marginBottom: 16 }}>
<SectionLabel>5 Dimensions</SectionLabel>
<h2 style={{ fontSize: 14, fontWeight: 700, margin: 0, color: "#f0eeff" }}>
Your Shape
</h2>
<p style={{ fontSize: 12, color: "#4a4d6a", margin: "4px 0 0" }}>
How you show up across five behavioral axes
</p>
</div>
{/* Pentagon chart */}
<PentagonChart dimensions={personality.dimensions} />
{/* All 5 flat bars */}
<div style={{ marginTop: 20 }}>
{personality.dimensions.map((d, i) => (
<RevealItem key={d.key} index={i}>
<FlatBar d={d} />
</RevealItem>
))}
</div>
{/* Shape narrative */}
{personality.shape_narrative && (
<div style={{ marginTop: 14, padding: "14px 16px",
background: "#0e1320", border: "1px solid #1e2438",
borderLeft: "3px solid rgba(29,78,216,0.4)",
borderRadius: 10 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: "#4a4865",
textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 8 }}>
What this shape means
</div>
<p style={{ margin: 0, fontSize: 13, color: "#8b89aa", lineHeight: 1.75 }}>
{personality.shape_narrative}
</p>
</div>
)}
</div>
</Reveal>
)}
{/* What's Changed */}
{((personality?.last_delta?.changes?.length ?? 0) > 0 || (trendLines?.length ?? 0) > 0) && (
<Reveal>
<WhatsChanged trendLines={trendLines} lastDelta={personality?.last_delta} />
</Reveal>
)}
{/* Signal Trends — collapsible */}
{chartData.length >= 2 && (
<Reveal>
<SignalTrends chartData={chartData} trendLines={trendLines} />
</Reveal>
)}
</div>
)
}