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 (
{children}
) } // ── 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 (
{cfg.icon}
{cfg.label}

{insight.text}

{insight.tip && (

{insight.tip}

)}
) } function MirrorFeed({ insights }) { const [expanded, setExpanded] = useState(false) if (!insights) return null if (!insights.length) { return (

Upload conversations across different contexts — patterns that span multiple sessions will appear here.

) } const hasMore = insights.length > FEED_MIN_VISIBLE const visible = expanded ? insights : insights.slice(0, FEED_MIN_VISIBLE) return (
{visible.map((insight, i) => ( ))}
{/* Gradient fade over last item with Show more pill */} {hasMore && !expanded && (
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", }} > Show more
)}
{/* Show less — simple link below when expanded */} {expanded && hasMore && ( )}
) } // ── Flat dimension bar ──────────────────────────────────────────── function FlatBar({ d }) { const score = useCountUp(d.score) return (
{d.name}
{score}
) } // ── 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 (
) } // ── 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 (
{contexts.map(ctx => ( ))}
Talk ratio
{data.talk_ratio}%
{norm && (
{withinNorm ? "✓" : "↑"} norm {norm[0]}–{norm[1]}%
)}
Speech rate
{data.wpm} wpm
Filler rate
{data.filler_rate}/100w
) } // ── What's Changed ──────────────────────────────────────────────── function WhatsChanged({ trendLines, lastDelta }) { const dimChanges = lastDelta?.changes || [] const signalTrends = trendLines || [] if (!dimChanges.length && !signalTrends.length) return null return (
Trends

What's Changed

Movement detected across your sessions

{dimChanges.map((c, i) => (
{c.direction === "up" ? "↑" : "↓"} {c.dimension} {" "}{c.direction === "up" ? "+" : ""}{c.diff}pts
))} {signalTrends.map((t, i) => (
{t.direction === "improved" ? "↗" : "↘"} {" "}{t.signal.replace(/_/g, " ")} {" "}{t.old}{t.unit} → {t.new}{t.unit}
))}
) } // ── 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 (
setOpen(v => !v)} style={{ padding: "14px 18px", display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer", borderBottom: open ? "1px solid #1e2438" : "none" }}> Signal Trends {open ? "▲ close" : "▼ expand"}
{open && (
{SIGNAL_OPTIONS.map(s => { const isActive = activeSignal === s.key return ( ) })}
{trendLines?.filter(t => t.signal === activeSignal).map((t, i) => (
{t.direction === "improved" ? "↗" : "↘"} {" "}{signalConfig?.label} {t.direction}: {t.old}{signalConfig?.unit} → {t.new}{signalConfig?.unit}
))}
`${v}${signalConfig?.unit || ""}`} /> [`${v}${signalConfig?.unit || ""}`, signalConfig?.label]} labelFormatter={(_, p) => p?.[0]?.payload?.date || ""} />
)}
) } // ── 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 (
Loading your profile…
) if (!profile || profile.insufficient_data) { return (

Your mirror is waiting

Upload your first conversation to see your behavioral profile.

) } 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 (
{/* Header */}

Your Behavioral Profile

{session_count} session{session_count > 1 ? "s" : ""} {completeness_label && ( <> · {completeness_label}
)}
{blind_spots?.length > 0 && (
{blindSpotsOpen && (
{blind_spots.map((spot, i) => (
·
{spot.label} — {spot.message}
))}
)}
)}
{/* Your Portrait — comes first, it's the centrepiece */} {personality && (
Your Portrait

{personality.paragraph}

{keywords.length > 0 && (
{keywords.map((kw, i) => ( {kw} ))}
)}
)} {/* Mirror Feed */}
Cross-Session

Mirror Feed

{/* Recurring Coaching */} {recurring_coaching?.length > 0 && (
Keeps Coming Up

Recurring Themes

Areas flagged in multiple sessions — these are your persistent development opportunities

{recurring_coaching.map((item, i) => { const accent = ["#f87171", "#fb923c", "#818cf8"][i] || "#8b89aa" return (
{item.area} {item.count} of {session_count} sessions
{item.tip && (

{item.tip}

)}
) })}
)} {/* You Across Contexts */} {hasContextData && (
Context Breakdown

You Across Contexts

How your patterns shift depending on the room

)} {/* Your Shape — pentagon + bars */} {personality?.dimensions?.length > 0 && (
5 Dimensions

Your Shape

How you show up across five behavioral axes

{/* Pentagon chart */} {/* All 5 flat bars */}
{personality.dimensions.map((d, i) => ( ))}
{/* Shape narrative */} {personality.shape_narrative && (
What this shape means

{personality.shape_narrative}

)}
)} {/* What's Changed */} {((personality?.last_delta?.changes?.length ?? 0) > 0 || (trendLines?.length ?? 0) > 0) && ( )} {/* Signal Trends — collapsible */} {chartData.length >= 2 && ( )}
) }