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 }) => (
))}
{/* 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?
setConfirmDeleteId(null)}
style={{ fontSize: 12, color: "#8b89aa", background: "none",
border: "none", cursor: "pointer", padding: "2px 8px" }}>
Cancel
handleDelete(s.session_id)} disabled={deleting}
style={{ fontSize: 12, color: "white", background: "#f87171",
border: "none", borderRadius: 5, padding: "3px 10px", fontWeight: 600,
cursor: deleting ? "not-allowed" : "pointer",
opacity: deleting ? 0.6 : 1 }}>
{deleting ? "Deleting…" : "Yes, delete"}
>
) : (
{ e.stopPropagation(); setConfirmDeleteId(s.session_id) }}
style={{ fontSize: 12, color: "#4a4865", background: "none",
border: "none", cursor: "pointer", padding: "2px 4px" }}>
Delete
)}
)
}
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.
loadSessions()} style={{
background: "rgba(29,78,216,0.1)", border: "1px solid rgba(29,78,216,0.3)",
borderRadius: 8, padding: "8px 20px", color: "#5b9cf6",
fontSize: 13, fontWeight: 500, cursor: "pointer",
}}>
Retry
)
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 (
setContextFilter(ctx)}
style={{ padding: "4px 12px", borderRadius: 20, fontSize: 11,
cursor: "pointer", fontWeight: isActive ? 600 : 400,
background: isActive ? hexToRgba(color, 0.12) : "#151922",
color: isActive ? color : "#8b89aa",
border: isActive ? `1px solid ${hexToRgba(color, 0.3)}` : "1px solid #1e2438",
transition: "all 0.15s" }}>
{ctx === "all" ? "All" : (CONTEXT_LABELS[ctx] || ctx)}
)
})}
{filtered.map((s, i) => (
))}
{/* Load more */}
{sessions.length < total && (
loadSessions(page + 1)}
disabled={loadingMore}
style={{
background: "none",
border: "1px solid #1e2438",
borderRadius: 8,
padding: "9px 24px",
fontSize: 13,
color: loadingMore ? "#4a4865" : "#8b89aa",
cursor: loadingMore ? "not-allowed" : "pointer",
fontFamily: "inherit",
transition: "border-color 0.15s, color 0.15s",
}}
onMouseEnter={e => { if (!loadingMore) { e.currentTarget.style.borderColor = "#2e3464"; e.currentTarget.style.color = "#f0eeff" } }}
onMouseLeave={e => { e.currentTarget.style.borderColor = "#1e2438"; e.currentTarget.style.color = loadingMore ? "#4a4865" : "#8b89aa" }}
>
{loadingMore
? "Loading…"
: `Load ${Math.min(PAGE_SIZE, total - sessions.length)} more`}
{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.
)}
)
}