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 (
)
}
// ── 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 && (
)}
)
}