Spaces:
Sleeping
Sleeping
File size: 5,544 Bytes
31abd7f 3efe926 31abd7f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import { useState, useEffect } from "react"
import api from "../lib/api"
import Reveal from "./Reveal"
const GROUPS = [
{
title: "Delivery",
sub: "pace & pauses — how it came out",
composites: ["speech_style", "vocal_arousal"],
},
{
title: "Relational Dynamics",
sub: "how you engaged the room",
composites: ["rapport", "power_balance", "turn_taking_courtesy"],
},
{
title: "Communication Effectiveness",
sub: "",
composites: ["fluency", "responsive_engagement"],
},
]
// Maps a continuous composite_z onto a 0–100 track position for the visual
// marker only — never printed as a number. z=0 sits at the midpoint; ±1.5
// reaches the track's ends. Purely illustrative, no claim of precision.
function zToPct(z) {
const clamped = Math.max(-1.5, Math.min(1.5, z))
return 50 + (clamped / 1.5) * 40
}
const STATUS_COLOR = {
"more than usual": "#5b9cf6",
"less than usual": "#7dd3c0",
"about your usual": "#8b89aa",
}
function SpectrumRow({ composite }) {
const [expanded, setExpanded] = useState(false)
if (!composite.is_steady) {
return (
<div style={{ padding: "14px 18px", borderBottom: "1px solid #1e2438" }}>
<div style={{ display: "flex", justifyContent: "space-between",
alignItems: "baseline", marginBottom: 9 }}>
<span style={{ fontSize: 13.5, fontWeight: 600, color: "#8b89aa" }}>
{composite.label}
</span>
<span style={{ fontSize: 11.5, color: "#4a4865", fontStyle: "italic" }}>
still forming
</span>
</div>
<div style={{ position: "relative", height: 5,
background: "repeating-linear-gradient(90deg, #1e2438 0 6px, transparent 6px 11px)",
borderRadius: 3 }} />
</div>
)
}
const pct = zToPct(composite.composite_z)
const color = STATUS_COLOR[composite.position] || "#5b9cf6"
return (
<div style={{ padding: "14px 18px", borderBottom: "1px solid #1e2438",
cursor: "pointer" }}
onClick={() => setExpanded(v => !v)}>
<div style={{ display: "flex", justifyContent: "space-between",
alignItems: "baseline", marginBottom: 9 }}>
<span style={{ fontSize: 13.5, fontWeight: 600, color: "#f0eeff" }}>
{composite.label}
</span>
<span style={{ fontSize: 11.5, fontWeight: 500, color }}>
{composite.position}
</span>
</div>
<div style={{ position: "relative", height: 20 }}>
<div style={{ position: "absolute", top: 8, left: 0, right: 0, height: 5,
background: "#1e2438", borderRadius: 3 }} />
<div style={{ position: "absolute", top: 2, width: 15, height: 15,
borderRadius: "50%", background: color, border: "2.5px solid #0a0c12",
left: `${pct}%`, transform: "translateX(-50%)",
boxShadow: `0 0 0 3px ${color}30` }} />
</div>
{expanded && (
<div style={{ marginTop: 12, padding: "12px 14px", background: "#131827",
border: "1px solid #1e2438", borderRadius: 9, fontSize: 12.5,
color: "#8b89aa", lineHeight: 1.7 }}>
{composite.components.filter(c => c.usable).map(c => (
<div key={c.signal} style={{ marginBottom: 4 }}>
<span style={{ color: "#f0eeff" }}>{c.signal.replace(/_/g, " ")}</span>
{": today "}{typeof c.today_value === "number" ? c.today_value.toFixed(2) : c.today_value}
{", your usual "}{c.historical_mean?.toFixed(2)}
</div>
))}
</div>
)}
</div>
)
}
export default function SessionSpectrum({ sessionId }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
useEffect(() => {
if (!sessionId) return
api.get(`/api/sessions/${sessionId}/spectrum`)
.then(res => setData(res.data))
.catch(() => setError(true))
.finally(() => setLoading(false))
}, [sessionId])
if (loading) return (
<div style={{ textAlign: "center", padding: 32, color: "#4a4865", fontSize: 13 }}>
Loading spectrum…
</div>
)
if (error || !data) return (
<div style={{ textAlign: "center", padding: 32, color: "#4a4865", fontSize: 13 }}>
Could not load Session Spectrum for this session.
</div>
)
return (
<Reveal>
<p style={{ fontSize: 13, color: "#8b89aa", marginBottom: 20, lineHeight: 1.65 }}>
Every mark below shows where <strong style={{ color: "#f0eeff" }}>this conversation</strong> fell
against <strong style={{ color: "#f0eeff" }}>your own</strong> usual range — never a grade, never
a comparison to anyone else. Signals need a handful of sessions before a range exists; until then
you'll see <em>still forming</em>.
</p>
{GROUPS.map(group => (
<div key={group.title} style={{ marginBottom: 22 }}>
<div style={{ marginBottom: 10 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: "#f0eeff" }}>{group.title}</span>
{group.sub && (
<span style={{ fontSize: 11, color: "#4a4865", marginLeft: 8 }}>{group.sub}</span>
)}
</div>
<div style={{ border: "1px solid #1e2438", borderRadius: 12,
background: "#151922", overflow: "hidden" }}>
{group.composites.map(key => (
<SpectrumRow key={key} composite={data.composites[key]} />
))}
</div>
</div>
))}
</Reveal>
)
}
|