Spaces:
Sleeping
Sleeping
Download frontend/src/components/HistoryView.jsx from Harsh200415/Mirror-Backend: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/Harsh200415/Mirror-Backend/resolve/main/frontend/src/components/HistoryView.jsx
- Command line
-
hf download hf://spaces/Harsh200415/Mirror-Backend/frontend/src/components/HistoryView.jsx
-
curl -L -o HistoryView.jsx https://huggingface.co/spaces/Harsh200415/Mirror-Backend/resolve/main/frontend/src/components/HistoryView.jsx
13.8 kB
| 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 ( | |
| <Reveal delay={Math.min(index * 80, 280)}> | |
| <div className="card" | |
| style={{ border: "1px solid #1e2438", borderRadius: 12, padding: 16, | |
| background: "#151922", boxShadow: "0 2px 16px rgba(0,0,0,0.3)" }}> | |
| <div onClick={() => 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 */} | |
| <div style={{ display: "flex", justifyContent: "space-between", | |
| alignItems: "center", marginBottom: 8 }}> | |
| <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}> | |
| {sessionTypes.map((t, i) => { | |
| const color = i === 0 ? contextColor(t) : "#8b89aa" | |
| return ( | |
| <span key={t} style={{ | |
| fontSize: 11, fontWeight: i === 0 ? 700 : 500, | |
| color, | |
| textTransform: "uppercase", letterSpacing: 0.4, | |
| background: i === 0 ? hexToRgba(color, 0.12) : "#131827", | |
| border: `1px solid ${i === 0 ? hexToRgba(color, 0.3) : "#1e2438"}`, | |
| padding: "2px 8px", borderRadius: 4, | |
| }}> | |
| {CONTEXT_LABELS[t] || t} | |
| </span> | |
| ) | |
| })} | |
| </div> | |
| <span style={{ fontSize: 11, color: "#4a4865" }}> | |
| {new Date(s.created_at).toLocaleDateString("en-IN", { | |
| day: "numeric", month: "short", year: "numeric" | |
| })} | |
| </span> | |
| </div> | |
| {/* Summary */} | |
| <p style={{ margin: "0 0 12px", fontSize: 13, color: "#8b89aa", lineHeight: 1.5 }}> | |
| {s.insights.summary_sentence} | |
| </p> | |
| {/* Signal stats */} | |
| <div style={{ display: "flex", gap: 16, flexWrap: "wrap", | |
| marginBottom: s.highlight ? 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 }) => ( | |
| <div key={label}> | |
| <div style={{ fontSize: 10, color: "#4a4865" }}>{label}</div> | |
| <div style={{ fontSize: 12, fontWeight: 600, color: "#f0eeff" }}>{value}</div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* 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 && ( | |
| <div style={{ display: "flex", alignItems: "baseline", gap: 6, paddingTop: 10, | |
| borderTop: "1px solid #1e2438" }}> | |
| <span style={{ fontSize: 11, fontWeight: 600, color: "#818cf8" }}> | |
| {s.highlight.label} | |
| </span> | |
| <span style={{ fontSize: 12, color: "#8b89aa" }}> | |
| {s.highlight.position} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| {/* Delete controls */} | |
| <div style={{ marginTop: 10, paddingTop: 8, borderTop: "1px solid #1e2438", | |
| display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8 }}> | |
| {confirmDeleteId === s.session_id ? ( | |
| <> | |
| <span style={{ fontSize: 12, color: "#8b89aa" }}>Delete this session?</span> | |
| <button onClick={() => setConfirmDeleteId(null)} | |
| style={{ fontSize: 12, color: "#8b89aa", background: "none", | |
| border: "none", cursor: "pointer", padding: "2px 8px" }}> | |
| Cancel | |
| </button> | |
| <button onClick={() => 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"} | |
| </button> | |
| </> | |
| ) : ( | |
| <button onClick={e => { e.stopPropagation(); setConfirmDeleteId(s.session_id) }} | |
| style={{ fontSize: 12, color: "#4a4865", background: "none", | |
| border: "none", cursor: "pointer", padding: "2px 4px" }}> | |
| Delete | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </Reveal> | |
| ) | |
| } | |
| 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 ( | |
| <div style={{ textAlign: "center", padding: 48, color: "#4a4865" }}> | |
| Loading sessions… | |
| </div> | |
| ) | |
| if (loadError) return ( | |
| <div style={{ textAlign: "center", padding: 48 }}> | |
| <div style={{ fontSize: 32, marginBottom: 12 }}>⚠️</div> | |
| <p style={{ color: "#f87171", fontSize: 14, marginBottom: 12 }}> | |
| Could not load sessions — the server may be starting up. | |
| </p> | |
| <button onClick={() => 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 | |
| </button> | |
| </div> | |
| ) | |
| if (sessions.length === 0) return ( | |
| <div style={{ textAlign: "center", padding: 48 }}> | |
| <div style={{ fontSize: 36, marginBottom: 12 }}>📭</div> | |
| <p style={{ color: "#8b89aa", fontSize: 14 }}>No sessions yet.</p> | |
| <p style={{ color: "#4a4865", fontSize: 13, marginTop: 4 }}> | |
| Record your first meeting to get started. | |
| </p> | |
| </div> | |
| ) | |
| 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 ( | |
| <div> | |
| {/* Header + filter */} | |
| <div style={{ display: "flex", justifyContent: "space-between", | |
| alignItems: "center", marginBottom: 16 }}> | |
| <p style={{ fontSize: 13, color: "#4a4865", margin: 0 }}> | |
| {sessions.length < total | |
| ? `${sessions.length} of ${total} sessions loaded` | |
| : `${filtered.length} of ${total} session${total !== 1 ? "s" : ""}`} | |
| </p> | |
| <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}> | |
| {contexts.map(ctx => { | |
| const isActive = contextFilter === ctx | |
| const color = ctx === "all" ? "#5b9cf6" : contextColor(ctx) | |
| return ( | |
| <button key={ctx} onClick={() => 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)} | |
| </button> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", flexDirection: "column", gap: 8 }}> | |
| {filtered.map((s, i) => ( | |
| <SessionCard | |
| key={s.session_id} | |
| s={s} | |
| index={i} | |
| onSelect={onSelect} | |
| confirmDeleteId={confirmDeleteId} | |
| setConfirmDeleteId={setConfirmDeleteId} | |
| handleDelete={handleDelete} | |
| deleting={deleting} | |
| /> | |
| ))} | |
| </div> | |
| {/* Load more */} | |
| {sessions.length < total && ( | |
| <div style={{ marginTop: 16, textAlign: "center" }}> | |
| <button | |
| onClick={() => 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`} | |
| </button> | |
| <p style={{ fontSize: 11, color: "#4a4865", marginTop: 8 }}> | |
| {total - sessions.length} more session{total - sessions.length !== 1 ? "s" : ""} not shown | |
| </p> | |
| </div> | |
| )} | |
| {sessions.length >= 3 && sessions.length >= total && ( | |
| <Reveal> | |
| <div style={{ marginTop: 20, padding: 14, | |
| background: "rgba(52,211,153,0.06)", | |
| border: "1px solid rgba(52,211,153,0.2)", | |
| borderRadius: 10, fontSize: 13, color: "#34d399" }}> | |
| ✨ {total} sessions recorded — your behavioral profile is active. | |
| Check the You tab. | |
| </div> | |
| </Reveal> | |
| )} | |
| </div> | |
| ) | |
| } | |