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 (
{composite.label} still forming
) } const pct = zToPct(composite.composite_z) const color = STATUS_COLOR[composite.position] || "#5b9cf6" return (
setExpanded(v => !v)}>
{composite.label} {composite.position}
{expanded && (
{composite.components.filter(c => c.usable).map(c => (
{c.signal.replace(/_/g, " ")} {": today "}{typeof c.today_value === "number" ? c.today_value.toFixed(2) : c.today_value} {", your usual "}{c.historical_mean?.toFixed(2)}
))}
)}
) } 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 (
Loading spectrum…
) if (error || !data) return (
Could not load Session Spectrum for this session.
) return (

Every mark below shows where this conversation fell against your own 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 still forming.

{GROUPS.map(group => (
{group.title} {group.sub && ( {group.sub} )}
{group.composites.map(key => ( ))}
))}
) }