import { useState, useEffect } from "react" import api from "../lib/api" import Reveal from "./Reveal" 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", } // Grouped by display label, not raw key — "social" and "casual" both render // as "Casual & Low-Stakes" and must share a color, same for every other // multi-key group above. const CONTEXT_COLORS = { social: "#5b9cf6", casual: "#5b9cf6", collaborative: "#2dd4bf", evaluative: "#f87171", job_interview: "#f87171", presentation: "#f87171", influential: "#fb923c", sales_call: "#fb923c", negotiation: "#f59e0b", adversarial: "#fb7185", disagreement: "#fb7185", developmental: "#a78bfa", feedback_conversation: "#a78bfa", coaching_call: "#a78bfa", support: "#34d399", intimate: "#e879f9", first_date: "#e879f9", meeting: "#94a3b8", } function contextColor(key) { return CONTEXT_COLORS[key] || "#5b9cf6" } function hexToRgba(hex, alpha) { const num = parseInt(hex.slice(1), 16) return `rgba(${(num >> 16) & 0xff}, ${(num >> 8) & 0xff}, ${num & 0xff}, ${alpha})` } function SessionCard({ s, index, onSelect, confirmDeleteId, setConfirmDeleteId, handleDelete, deleting }) { const sessionTypes = (s.insights?.conversation_types || [s.context]) // 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", session_id: s.session_id, fingerprint: s.fingerprint || null, })} style={{ cursor: "pointer" }}> {/* Top row */}
{sessionTypes.map((t, i) => { const color = i === 0 ? contextColor(t) : "#8b89aa" return ( {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 */}
{[ { 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}
))}
{/* Self-relative highlight — one steady composite, "more/less than your usual", never a number, never a grade. None shown if nothing is steady yet for this user+context (honest, not padded). */} {s.highlight && (
{s.highlight.label} {s.highlight.position}
)}
{/* 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 data = res.data const newSessions = Array.isArray(data) ? data : (data.sessions ?? []) const t = Array.isArray(data) ? data.length : (data.total ?? 0) 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.

Record your first meeting 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 const color = ctx === "all" ? "#5b9cf6" : contextColor(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 You tab.
)}
) }