import { useState, useEffect } from "react" import api from "../lib/api" import Reveal from "./Reveal" import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, } from "recharts" // Palette for up to 6 speakers const SPEAKER_COLORS = ["#3b82f6", "#8b5cf6", "#34d399", "#f59e0b", "#f87171", "#0891b2"] function speakerDisplayName(id, detectedSpeaker) { if (id === detectedSpeaker) return "You" const num = parseInt(id.replace("SPEAKER_", ""), 10) return isNaN(num) ? id : `Participant ${num}` } function formatMin(s) { return `${Math.floor(s / 60)}m` } function SpeakerTimeline({ speakersTimeline, detectedSpeaker }) { const speakers = Object.keys(speakersTimeline || {}) if (!speakers.length) return null // Merge all per-speaker windows into a unified time axis const windowMap = {} for (const sp of speakers) { for (const w of speakersTimeline[sp]) { const key = w.window_start_s if (!windowMap[key]) windowMap[key] = { t: key } windowMap[key][sp] = w.speech_rate_wpm } } const chartData = Object.values(windowMap).sort((a, b) => a.t - b.t) if (chartData.length < 2) return null return (
Who spoke when — speech rate (wpm)
`at ${formatMin(v)}`} formatter={(v, name) => [`${Math.round(v)} wpm`, speakerDisplayName(name, detectedSpeaker)]} /> speakerDisplayName(name, detectedSpeaker)} wrapperStyle={{ fontSize: 11, color: "#8b89aa" }} /> {speakers.map((sp, i) => ( ))}
) } 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 SCORE_COLORS = ["#f87171", "#fb923c", "#f59e0b", "#34d399", "#10b981"] function MiniScoreBar({ score }) { if (!score || score < 1) return null const color = SCORE_COLORS[score - 1] || "#4a4865" return (
{Array.from({ length: 5 }).map((_, i) => (
))}
) } function SessionCard({ s, index, onSelect, confirmDeleteId, setConfirmDeleteId, handleDelete, deleting }) { const [showTimeline, setShowTimeline] = useState(false) const dims = s.dimensions || {} const dimSummary = [ { label: "Emotional", score: dims.emotional_state?.confidence?.score }, { label: "Rapport", score: dims.relational_dynamics?.rapport?.score }, { label: "Clarity", score: dims.communication_effectiveness?.clarity?.score }, ].filter(d => d.score != null) const sessionTypes = (s.insights?.conversation_types || [s.context]) const hasTimeline = s.speakers_timeline && Object.keys(s.speakers_timeline).length > 0 // Talk split percentages const dur = s.signals.session_duration_s || 1 const userPct = Math.round((s.signals.talk_ratio.user_speaking_time_s / dur) * 100) const otherPct = Math.round((s.signals.talk_ratio.other_speaking_time_s / dur) * 100) const silPct = Math.max(0, 100 - userPct - otherPct) return (
onSelect({ signals: s.signals, insights: s.insights, dimensions: s.dimensions || {}, filename: s.filename || "recording", detected_speaker: s.detected_speaker || "SPEAKER_00", speaker_confirmed: s.speaker_confirmed || false, session_id: s.session_id, available_speakers: s.available_speakers || [], fingerprint: s.fingerprint || null, })} style={{ cursor: "pointer" }}> {/* Top row */}
{sessionTypes.map((t, i) => ( {CONTEXT_LABELS[t] || t} ))}
{new Date(s.created_at).toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}
{/* Summary */}

{s.insights.summary_sentence}

{/* Signal stats */}
0 ? 12 : 0 }}> {[ { label: "You", value: `${userPct}%` }, { label: "Others", value: `${otherPct}%` }, { label: "Silence", value: `${silPct}%` }, { label: "WPM", value: `${s.signals.speech_rate.overall_wpm}` }, { label: "Fillers", value: `${s.signals.filler_words.rate_per_100_words}/100w` }, { label: "Duration", value: `${Math.round(dur / 60)}m` }, ].map(({ label, value }) => (
{label}
{value}
))}
{/* Dimension score bars */} {dimSummary.length > 0 && (
{dimSummary.map(({ label, score }) => (
{label}
))}
)}
{/* Speaker timeline toggle */} {hasTimeline && (
{showTimeline && ( )}
)} {/* Delete controls */}
{confirmDeleteId === s.session_id ? ( <> Delete this session? ) : ( )}
) } const PAGE_SIZE = 10 export default function HistoryView({ onSelect, active = false }) { const [sessions, setSessions] = useState([]) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(false) const [confirmDeleteId, setConfirmDeleteId] = useState(null) const [deleting, setDeleting] = useState(false) const [contextFilter, setContextFilter] = useState("all") const [page, setPage] = useState(0) const [total, setTotal] = useState(0) const [loadingMore, setLoadingMore] = useState(false) const handleDelete = async (sessionId) => { setDeleting(true) try { await api.delete(`/api/sessions/${sessionId}`) setSessions(prev => prev.filter(s => s.session_id !== sessionId)) setTotal(prev => prev - 1) } catch (e) { console.error("Delete failed:", e) } finally { setDeleting(false) setConfirmDeleteId(null) } } const loadSessions = (pageNum = 0) => { if (pageNum === 0) { setLoading(true) setLoadError(false) setSessions([]) setPage(0) } else { setLoadingMore(true) } api.get(`/api/sessions?page=${pageNum}&page_size=${PAGE_SIZE}`) .then(res => { const { sessions: newSessions, total: t } = res.data setSessions(prev => pageNum === 0 ? newSessions : [...prev, ...newSessions]) setTotal(t) setPage(pageNum) }) .catch(err => { console.error(err) if (pageNum === 0) setLoadError(true) }) .finally(() => { setLoading(false); setLoadingMore(false) }) } useEffect(() => { if (!active) return loadSessions(0) }, [active]) if (loading) return (
Loading sessions…
) if (loadError) return (
⚠️

Could not load sessions — the server may be starting up.

) if (sessions.length === 0) return (
📭

No sessions yet.

Upload your first conversation to get started.

) const getTypes = (s) => s.insights?.conversation_types || [s.context] const contexts = ["all", ...new Set(sessions.flatMap(s => getTypes(s)))] const filtered = contextFilter === "all" ? sessions : sessions.filter(s => getTypes(s).includes(contextFilter)) return (
{/* Header + filter */}

{sessions.length < total ? `${sessions.length} of ${total} sessions loaded` : `${filtered.length} of ${total} session${total !== 1 ? "s" : ""}`}

{contexts.map(ctx => { const isActive = contextFilter === ctx return ( ) })}
{filtered.map((s, i) => ( ))}
{/* Load more */} {sessions.length < total && (

{total - sessions.length} more session{total - sessions.length !== 1 ? "s" : ""} not shown

)} {sessions.length >= 3 && sessions.length >= total && (
✨ {total} sessions recorded — your behavioral profile is active. Check the Profile tab.
)}
) }