import { useState } from "react"
import Reveal, { RevealItem } from "./Reveal"
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts"
import api from "../lib/api"
const G = "linear-gradient(135deg, #1d4ed8 0%, #0891b2 100%)"
const SCORE_COLORS = ["#f87171", "#fb923c", "#f59e0b", "#34d399", "#10b981"]
function ScoreBar({ score, max = 5 }) {
const color = SCORE_COLORS[score - 1] || "#4a4865"
return (
setExpanded(!expanded)}
style={{ padding: "14px 16px", cursor: "pointer",
display: "flex", justifyContent: "space-between", alignItems: "center" }}>
{icon}
{title}
{expanded ? "β²" : "βΌ"}
{items.map(({ label, score, labelText }) => (
))}
{expanded && narrative && (
{narrative}
)}
)
}
function CoachingCard({ suggestion }) {
const priorityColors = { 1: "#f87171", 2: "#fb923c", 3: "#818cf8" }
const color = priorityColors[suggestion.priority] || "#8b89aa"
return (
{obs.signal.replace(/_/g, " ")}
{obs.observation}
π {obs.resonance_prompt}
{resonance === null ? (
Does this resonate?
{[["Yes", "yes"], ["Somewhat", "somewhat"], ["No", "no"]].map(([label, value]) => (
{
setResonance(label)
if (sessionId) {
try {
const form = new FormData()
form.append("signal", obs.signal)
form.append("response", value)
await api.post(`/api/sessions/${sessionId}/resonance`, form)
} catch {}
}
}}
style={{ padding: "5px 14px", border: "1px solid #1e2438",
borderRadius: 20, background: "#131827", cursor: "pointer",
fontSize: 12, color: "#8b89aa" }}>
{label}
))}
) : (
β Noted β thanks.
)}
)
}
const speakerLabel = (id) => {
if (!id) return "Unknown"
const num = parseInt(id.replace("SPEAKER_", ""), 10)
return isNaN(num) ? id : `Speaker ${num + 1}`
}
const LABELS = {
social: "Social", collaborative: "Collaborative", evaluative: "Evaluative",
influential: "Influential", negotiation: "Negotiation", adversarial: "Adversarial",
developmental: "Developmental", support: "Support", intimate: "Intimate",
casual: "Casual", meeting: "Meeting", job_interview: "Job Interview",
disagreement: "Disagreement", presentation: "Presentation",
}
export default function ResultsView({ results, onBack }) {
const [liveResults, setLiveResults] = useState(results)
const [reanalyzing, setReanalyzing] = useState(false)
const [reanalyzeError, setReanalyzeError] = useState("")
const [showSpeakerSwitch, setShowSpeakerSwitch] = useState(false)
const [activeTab, setActiveTab] = useState("overview")
const { signals, insights, dimensions, filename, detected_speaker,
session_id, voiceprint_confidence, fingerprint } = liveResults
const confPct = voiceprint_confidence != null ? Math.round(voiceprint_confidence * 100) : null
const confLabel = confPct == null ? null : confPct >= 55 ? "high" : confPct >= 40 ? "medium" : "low"
const confColor = confLabel === "high" ? "#34d399" : confLabel === "medium" ? "#f59e0b" : "#f87171"
const formatTime = (s) => {
const m = Math.floor(s / 60)
const sec = Math.round(s % 60)
return `${m}:${sec.toString().padStart(2, "0")}`
}
const talkPct = Math.round(signals.talk_ratio.user_ratio * 100)
const duration = Math.round(signals.session_duration_s / 60)
const totalDur = signals.session_duration_s || 1
const userTalkPct = Math.round((signals.talk_ratio.user_speaking_time_s / totalDur) * 100)
const otherTalkPct = Math.round((signals.talk_ratio.other_speaking_time_s / totalDur) * 100)
const silencePct = Math.max(0, 100 - userTalkPct - otherTalkPct)
const handleReanalyze = async (newSpeaker) => {
if (!session_id) return
setShowSpeakerSwitch(false); setReanalyzing(true); setReanalyzeError("")
try {
const form = new FormData()
form.append("confirmed_speaker", newSpeaker)
const res = await api.post(`/api/sessions/${session_id}/reanalyze`, form)
setLiveResults(res.data)
} catch (e) {
setReanalyzeError(e.response?.data?.detail || e.message || "Switch failed.")
} finally {
setReanalyzing(false)
}
}
const tabs = ["overview", "dimensions", "coaching", "signals"]
const otherSpeakers = (liveResults.available_speakers || []).filter(s => s !== detected_speaker)
return (
β Back
{/* Speaker bar */}
{filename && π {filename} Β· }
Analysis for {speakerLabel(detected_speaker)}
{confPct != null && (
Β· {confPct}% match
)}
{otherSpeakers.length > 0 && !reanalyzing && (
setShowSpeakerSwitch(v => !v)}
style={{ fontSize: 12, color: "#8b89aa", background: "#151922",
border: "1px solid #1e2438", borderRadius: 20,
padding: "3px 12px", cursor: "pointer" }}>
{reanalyzing ? "Switchingβ¦" : "Not you?"}
{showSpeakerSwitch && !reanalyzing && (
{otherSpeakers.map(s => (
handleReanalyze(s)}
style={{ display: "block", width: "100%", padding: "10px 14px",
textAlign: "left", background: "none", border: "none",
borderBottom: "1px solid #1e2438",
cursor: "pointer", fontSize: 13, color: "#f0eeff" }}>
Switch to {speakerLabel(s)}
))}
)}
)}
{/* Low voiceprint confidence nudge */}
{(confLabel === "low" || (voiceprint_confidence == null && !liveResults.speaker_confirmed)) && (
β Voice match is low β insights may be based on the wrong speaker.
{otherSpeakers.length > 0
? " Try switching speakers using the button above."
: " Re-enroll your voice (Account β Retrain your voice) for better accuracy next time."}
)}
{reanalyzeError && (
β οΈ {reanalyzeError}
)}
{/* Mirror updated banner */}
{session_id && (
β
Session saved β your mirror has been updated.
)}
{/* Tabs */}
{tabs.map(tab => (
setActiveTab(tab)}
className={`nav-tab${activeTab === tab ? " active" : ""}`}
style={{ background: "none", border: "none", cursor: "pointer",
padding: "8px 14px", fontSize: 13, textTransform: "capitalize",
fontWeight: activeTab === tab ? 600 : 400,
color: activeTab === tab ? "#5b9cf6" : "#8b89aa" }}>
{tab}
))}
{/* ββ OVERVIEW TAB ββ */}
{activeTab === "overview" && (
{/* Type chips */}
{(() => {
const types = insights.conversation_types || []
if (types.length === 0) return null
return (
{types.map((t, i) => (
{LABELS[t] || t}
))}
auto-detected
)
})()}
{/* Conversation Summary β what it was about */}
{insights.conversation_summary && (
The Conversation
{insights.conversation_summary}
)}
{/* Your Perspective β what the user specifically said/contributed */}
{insights.user_perspective && (
Your Perspective
{insights.user_perspective}
)}
{/* Notable pattern */}
{insights.notable_pattern && (
Notable Pattern
{insights.notable_pattern}
)}
{/* Behavioral Observations β moved from Signals tab */}
{insights.observations?.length > 0 && (
Behavioral Observations
{insights.observations.map((obs, i) => (
))}
)}
{/* Key metrics β gradient numbers */}
{[
{ label: "Duration", value: `${duration}`, unit: "m" },
{ label: "Speech rate", value: `${signals.speech_rate.overall_wpm}`, unit: "wpm" },
{ label: "Fillers", value: `${signals.filler_words.rate_per_100_words}`, unit: "/100w" },
].map(({ label, value, unit }) => (
))}
{/* Talk split bar β You / Others / Silence */}
Talk split
{[
{ label: "You", pct: userTalkPct, color: "#3b82f6" },
{ label: "Others", pct: otherTalkPct, color: "#8b5cf6" },
{ label: "Silence", pct: silencePct, color: "#4a4865" },
].map(({ label, pct, color }) => (
))}
{/* Secondary metrics */}
{[
{ label: "Interruptions given", value: `${signals.interruptions.user_interrupted_other}x` },
{ label: "Interruptions received", value: `${signals.interruptions.user_was_interrupted}x` },
{ label: "Questions asked", value: `${signals.questions.user_questions_asked}` },
{ label: "Energy trend", value: signals.vocal_energy.trend === "insufficient_data"
? "β" : signals.vocal_energy.trend },
].map(({ label, value }) => (
))}
)}
{/* ββ DIMENSIONS TAB ββ */}
{activeTab === "dimensions" && dimensions && (
Scores are probabilistic proxies based on behavioral signals β
not psychological diagnoses. Expand any card to read the interpretation.
{dimensions.emotional_state && (
)}
{dimensions.relational_dynamics && (
)}
{dimensions.communication_effectiveness && (
)}
{/* Conversation Arc β moved from Overview */}
{dimensions?.conversation_arc && (
Conversation Arc
{[
{ label: "Opening vs Closing", value: dimensions.conversation_arc.opening_vs_closing?.label },
{ label: "Tension", value: dimensions.conversation_arc.tension_arc?.label },
{ label: "Who Drove", value: dimensions.conversation_arc.who_drove?.label },
{ label: "Resolution", value: dimensions.conversation_arc.resolution_proxy?.label },
].map(({ label, value }) => (
))}
{dimensions.conversation_arc.turning_point?.detected && (
β‘ {dimensions.conversation_arc.turning_point.detail}
)}
{insights.dimension_narrative?.conversation_arc && (
{insights.dimension_narrative.conversation_arc}
)}
)}
)}
{/* ββ COACHING TAB ββ */}
{activeTab === "coaching" && (
Specific suggestions based on patterns observed in this conversation.
Ranked by potential impact.
{insights.coaching_suggestions?.map((s, i) => (
))}
{(!insights.coaching_suggestions || insights.coaching_suggestions.length === 0) && (
No coaching suggestions generated for this session.
)}
)}
{/* ββ SIGNALS TAB ββ */}
{activeTab === "signals" && (
{signals.timeline?.length > 1 && (
Speech Rate Over Time
`At ${formatTime(v)}`}
formatter={v => [`${Math.round(v)} wpm`, "Speech rate"]} />
{signals.speech_acceleration.trend !== "insufficient_data" && (
Trend: {signals.speech_acceleration.trend}
{signals.speech_acceleration.delta_wpm &&
` (${signals.speech_acceleration.delta_wpm > 0 ? "+" : ""}${signals.speech_acceleration.delta_wpm} wpm)`}
)}
)}
All Signals
{[
{ label: "Silence ratio", value: `${Math.round(signals.silence_ratio.silence_ratio * 100)}%` },
{ label: "Vocab richness", value: signals.vocabulary_richness.type_token_ratio?.toFixed(2) || "β" },
{ label: "Longest turn", value: `${signals.monologue.longest_turn_s}s` },
{ label: "Avg response latency", value: `${signals.pauses.response_latency.mean_s}s` },
{ label: "Within-turn pauses", value: signals.pauses.within_turn_pauses.count },
{ label: "Your turns", value: signals.turn_dynamics.user_turns },
].map(({ label, value }) => (
))}
)}
{/* Disclaimer */}
Note: All scores and observations are
probabilistic proxies based on acoustic and linguistic patterns. They are not validated
psychological assessments. Use as prompts for self-reflection only.
)
}