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 (
{Array.from({ length: max }).map((_, i) => (
))}
) } function DimensionCard({ title, icon, items, narrative }) { const [expanded, setExpanded] = useState(false) return (
setExpanded(!expanded)} style={{ padding: "14px 16px", cursor: "pointer", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
{icon} {title}
{expanded ? "β–²" : "β–Ό"}
{items.map(({ label, score, labelText }) => (
{label}
{labelText}
))}
{expanded && narrative && (
{narrative}
)}
) } function CoachingCard({ suggestion }) { const priorityColors = { 1: "#f87171", 2: "#fb923c", 3: "#818cf8" } const color = priorityColors[suggestion.priority] || "#8b89aa" return (
#{suggestion.priority} β€” {suggestion.area}
{suggestion.issue}
πŸ’‘ {suggestion.suggestion}
Why it matters: {suggestion.why_it_matters}
) } function ObservationCard({ obs, sessionId }) { const [resonance, setResonance] = useState(null) const signalColors = { talk_ratio: "#818cf8", speech_rate: "#5b9cf6", speech_acceleration: "#f472b6", pauses: "#f59e0b", interruptions: "#f87171", filler_words: "#fb923c", vocal_energy: "#0891b2", questions: "#34d399", monologue: "#818cf8", vocabulary_richness: "#a3e635", silence_ratio: "#8b89aa", pitch: "#0891b2", engagement: "#5b9cf6" } const color = signalColors[obs.signal] || "#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]) => ( ))}
) : (
βœ“ 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 (
{/* Speaker bar */}
{filename && πŸ“ {filename} Β·} Analysis for {speakerLabel(detected_speaker)} {confPct != null && ( Β· {confPct}% match )}
{otherSpeakers.length > 0 && !reanalyzing && (
{showSpeakerSwitch && !reanalyzing && (
{otherSpeakers.map(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 => ( ))}
{/* ── 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 }) => (
{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 }) => (
{label} {pct}%
))}
{/* 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 }) => (
{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 }) => (
{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 }) => (
{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.
) }