Merge gap-d-dead-components: wire up TelemetryReadout and EntityCard

This commit is contained in:
Indiana
2026-07-23 00:33:27 +00:00
3 changed files with 129 additions and 192 deletions

View File

@@ -1,79 +1,104 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { SpiritEntity } from '../lib/types' import type { SpiritEntity } from '../lib/types'
import { GhostGlyph } from './GhostGlyph'
export type EntityCardProps = { export type EntityCardProps = {
entity: SpiritEntity | null entity: SpiritEntity | null
isNew: boolean isNew: boolean
/** Whether the collapsible dossier body is expanded. */
open: boolean
/** Toggle the collapsed/expanded state (ignored while there's no entity). */
onToggleOpen: () => void
} }
const RARITY_CLASS: Record<string, string> = { /**
common: 'rarity-common', * Occult dossier of the currently-summoned spirit, wax-seal vibe.
uncommon: 'rarity-uncommon', * Styling (`.spirit-dossier` / `.dossier-*` / `.rarity-badge` / …) lives in
rare: 'rarity-rare', * SeancePage.css alongside the rest of the side column.
mythic: 'rarity-mythic', */
} export function EntityCard({ entity, isNew, open, onToggleOpen }: EntityCardProps) {
/** Occult dossier of the currently-summoned spirit, wax-seal vibe. */
export function EntityCard({ entity, isNew }: EntityCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
if (!entity) { if (!entity) {
return ( return (
<aside className="entity-card empty"> <section className="spirit-dossier empty">
<h3 className="ec-title">{t('seance.entity.title')}</h3> <div className="dossier-body">
<div className="ec-none"> <p className="dossier-none">{t('seance.entity.none')}</p>
<div className="ec-none-sigil">⟁</div> <p className="dossier-nonehint dim">{t('seance.entity.noneHint')}</p>
<p>{t('seance.entity.none')}</p>
<p className="dim">{t('seance.entity.noneHint')}</p>
</div> </div>
</aside> </section>
) )
} }
return ( return (
<aside className="entity-card"> <section className={`spirit-dossier ${open ? 'open' : 'closed'}`}>
<div className="ec-wax" aria-hidden="true"> <button type="button" className="dossier-header" onClick={onToggleOpen} aria-expanded={open}>
{entity.name.slice(0, 1).toUpperCase()} <span className="dossier-title">{t('seance.entity.title')}</span>
</div> <span className="dossier-chevron" aria-hidden>
<h3 className="ec-title">{t('seance.entity.title')}</h3> {open ? '▾' : '◂'}
<div className="ec-head"> </span>
<GhostGlyph hue={entity.visual.hue} form={entity.visual.form} size={64} /> </button>
<div> {open && (
<div className="ec-name glitch-hover">{entity.name}</div> <div className="dossier-body">
<div className="ec-epithet">{entity.epithet}</div> <h3 className="dossier-name" style={{ color: `hsl(${entity.visual.hue} 85% 78%)` }}>
<span className={`rarity-badge ${RARITY_CLASS[entity.rarity] ?? ''}`}> {entity.name}
{t(`seance.entity.rarity.${entity.rarity}`)} </h3>
</span> <p className="dossier-epithet">{entity.epithet}</p>
</div> <div className="dossier-badges">
</div> <span className={`rarity-badge rarity-${entity.rarity}`}>
<div className="ec-contact"> {t(`seance.entity.rarity.${entity.rarity}`)}
{isNew ? t('seance.entity.isNew') : t('seance.entity.known', { count: entity.contact_count })} </span>
</div> {isNew ? (
<div className="ec-section"> <span className="dossier-badge badge-new">{t('seance.entity.isNew')}</span>
<h4>{t('seance.entity.persona')}</h4> ) : (
<p className="ec-persona">{entity.persona}</p> <span className="dossier-badge badge-known">
</div> {t('seance.entity.known', { count: entity.contact_count })}
{entity.quotes.length > 0 && ( </span>
<div className="ec-section"> )}
<h4>{t('seance.entity.quotes')}</h4> </div>
<ul className="ec-quotes">
{entity.quotes.slice(0, 4).map((q, i) => ( <div className="dossier-section">
<li key={i}>“{q}”</li> <h4>{t('seance.entity.persona')}</h4>
))} <p className="dossier-persona">{entity.persona}</p>
</ul> </div>
<div className="dossier-section">
<h4>{t('seance.entity.voice')}</h4>
<dl className="voice-params">
<div>
<dt>id</dt>
<dd>{entity.voice.voice_id}</dd>
</div>
<div>
<dt>pitch</dt>
<dd>{entity.voice.pitch.toFixed(2)}</dd>
</div>
<div>
<dt>rate</dt>
<dd>{entity.voice.rate.toFixed(2)}</dd>
</div>
<div>
<dt>noise</dt>
<dd>{entity.voice.noise.toFixed(2)}</dd>
</div>
<div>
<dt>echo</dt>
<dd>{entity.voice.echo.toFixed(2)}</dd>
</div>
</dl>
</div>
{entity.quotes.length > 0 && (
<div className="dossier-section">
<h4>{t('seance.entity.quotes')}</h4>
<ul className="dossier-quotes">
{entity.quotes.slice(0, 4).map((q, i) => (
<li key={i}>“{q}”</li>
))}
</ul>
</div>
)}
</div> </div>
)} )}
<div className="ec-section"> </section>
<h4>{t('seance.entity.voice')}</h4>
<div className="ec-voice">
<span>id {entity.voice.voice_id}</span>
<span>pitch {entity.voice.pitch.toFixed(2)}</span>
<span>rate {entity.voice.rate.toFixed(2)}</span>
<span>noise {entity.voice.noise.toFixed(2)}</span>
<span>echo {entity.voice.echo.toFixed(2)}</span>
</div>
</div>
</aside>
) )
} }

View File

@@ -1,28 +1,47 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { Telemetry } from '../lib/types' import type { Telemetry } from '../lib/types'
/** Flickering terminal readout of live wire telemetry. */ /**
* Flickering terminal readout of live wire telemetry.
* Styling (`.panel-title` / `.telemetry-grid` / `.tel-row` / …) lives in
* SeancePage.css alongside the other mode-panel styles.
*/
export function TelemetryReadout({ telemetry }: { telemetry: Telemetry | null }) { export function TelemetryReadout({ telemetry }: { telemetry: Telemetry | null }) {
const { t } = useTranslation() const { t } = useTranslation()
const rows: Array<[string, string]> = telemetry
? [
[t('seance.telemetry.jitter'), `${telemetry.jitter_bytes_per_s.toFixed(1)} ${t('seance.telemetry.units.bytes')}`],
[t('seance.telemetry.latencyVar'), `${telemetry.latency_variance_ms.toFixed(2)} ${t('seance.telemetry.units.ms')}`],
[t('seance.telemetry.latencyMean'), `${telemetry.latency_mean_ms.toFixed(2)} ${t('seance.telemetry.units.ms')}`],
[t('seance.telemetry.dns'), `${telemetry.dns_ms.toFixed(1)} ${t('seance.telemetry.units.ms')}`],
]
: []
return ( return (
<div className="telemetry"> <>
<h4 className="telemetry-title">{t('seance.telemetry.title')}</h4> <h4 className="panel-title">{t('seance.telemetry.title')}</h4>
{rows.length === 0 && <div className="telemetry-row dim">— — —</div>} {telemetry ? (
{rows.map(([label, value]) => ( <div className="telemetry-grid">
<div className="telemetry-row flicker" key={label}> <div className="tel-row">
<span className="telemetry-label">{label}</span> <span className="tel-key">{t('seance.telemetry.jitter')}</span>
<span className="telemetry-value">{value}</span> <span className="tel-val flicker-soft">
{telemetry.jitter_bytes_per_s.toFixed(1)} {t('seance.telemetry.units.bytes')}
</span>
</div>
<div className="tel-row">
<span className="tel-key">{t('seance.telemetry.latencyVar')}</span>
<span className="tel-val flicker-soft">
{telemetry.latency_variance_ms.toFixed(2)} {t('seance.telemetry.units.ms')}
</span>
</div>
<div className="tel-row">
<span className="tel-key">{t('seance.telemetry.latencyMean')}</span>
<span className="tel-val flicker-soft">
{telemetry.latency_mean_ms.toFixed(1)} {t('seance.telemetry.units.ms')}
</span>
</div>
<div className="tel-row">
<span className="tel-key">{t('seance.telemetry.dns')}</span>
<span className="tel-val flicker-soft">
{telemetry.dns_ms.toFixed(1)} {t('seance.telemetry.units.ms')}
</span>
</div>
</div> </div>
))} ) : (
</div> <p className="dim flicker">{t('common.loading')}</p>
)}
</>
) )
} }

View File

@@ -22,6 +22,8 @@ import { PlanchetteBoard } from '../components/PlanchetteBoard'
import { MatrixView } from '../components/MatrixView' import { MatrixView } from '../components/MatrixView'
import { GraphView } from '../components/GraphView' import { GraphView } from '../components/GraphView'
import { Transcript } from '../components/Transcript' import { Transcript } from '../components/Transcript'
import { EntityCard } from '../components/EntityCard'
import { TelemetryReadout } from '../components/TelemetryReadout'
import { PlanchetteMachine } from '../lib/planchette' import { PlanchetteMachine } from '../lib/planchette'
import type { PlanchetteSnapshot } from '../lib/planchette' import type { PlanchetteSnapshot } from '../lib/planchette'
import { EvpListener } from '../lib/evp' import { EvpListener } from '../lib/evp'
@@ -335,90 +337,12 @@ function SeanceSession({ username }: { username: string }) {
</section> </section>
<aside className="seance-side"> <aside className="seance-side">
{entity ? ( <EntityCard
<section className={`spirit-dossier ${dossierOpen ? 'open' : 'closed'}`}> entity={entity}
<button isNew={state.entityIsNew}
type="button" open={dossierOpen}
className="dossier-header" onToggleOpen={() => setDossierOpen((o) => !o)}
onClick={() => setDossierOpen((o) => !o)} />
aria-expanded={dossierOpen}
>
<span className="dossier-title">{t('seance.entity.title')}</span>
<span className="dossier-chevron" aria-hidden>
{dossierOpen ? '▾' : '◂'}
</span>
</button>
{dossierOpen && (
<div className="dossier-body">
<h3 className="dossier-name" style={{ color: `hsl(${entity.visual.hue} 85% 78%)` }}>
{entity.name}
</h3>
<p className="dossier-epithet">{entity.epithet}</p>
<div className="dossier-badges">
<span className={`rarity-badge rarity-${entity.rarity}`}>
{t(`seance.entity.rarity.${entity.rarity}`)}
</span>
{state.entityIsNew ? (
<span className="dossier-badge badge-new">{t('seance.entity.isNew')}</span>
) : (
<span className="dossier-badge badge-known">
{t('seance.entity.known', { count: entity.contact_count })}
</span>
)}
</div>
<div className="dossier-section">
<h4>{t('seance.entity.persona')}</h4>
<p className="dossier-persona">{entity.persona}</p>
</div>
<div className="dossier-section">
<h4>{t('seance.entity.voice')}</h4>
<dl className="voice-params">
<div>
<dt>id</dt>
<dd>{entity.voice.voice_id}</dd>
</div>
<div>
<dt>pitch</dt>
<dd>{entity.voice.pitch.toFixed(2)}</dd>
</div>
<div>
<dt>rate</dt>
<dd>{entity.voice.rate.toFixed(2)}</dd>
</div>
<div>
<dt>noise</dt>
<dd>{entity.voice.noise.toFixed(2)}</dd>
</div>
<div>
<dt>echo</dt>
<dd>{entity.voice.echo.toFixed(2)}</dd>
</div>
</dl>
</div>
{entity.quotes.length > 0 && (
<div className="dossier-section">
<h4>{t('seance.entity.quotes')}</h4>
<ul className="dossier-quotes">
{entity.quotes.slice(0, 4).map((q, i) => (
<li key={i}>“{q}”</li>
))}
</ul>
</div>
)}
</div>
)}
</section>
) : (
<section className="spirit-dossier empty">
<div className="dossier-body">
<p className="dossier-none">{t('seance.entity.none')}</p>
<p className="dossier-nonehint dim">{t('seance.entity.noneHint')}</p>
</div>
</section>
)}
<section className="mode-panel"> <section className="mode-panel">
{state.mode === 'wire' && <WirePanel />} {state.mode === 'wire' && <WirePanel />}
@@ -519,41 +443,10 @@ function ToastCard({ toast, onDismiss }: { toast: Toast; onDismiss: (id: number)
function WirePanel() { function WirePanel() {
const { state } = useSeance() const { state } = useSeance()
const { t } = useTranslation() const { t } = useTranslation()
const tel = state.telemetry
return ( return (
<div className="wire-panel"> <div className="wire-panel">
<h4 className="panel-title">{t('seance.telemetry.title')}</h4> <TelemetryReadout telemetry={state.telemetry} />
{tel ? (
<div className="telemetry-grid">
<div className="tel-row">
<span className="tel-key">{t('seance.telemetry.jitter')}</span>
<span className="tel-val flicker-soft">
{tel.jitter_bytes_per_s.toFixed(1)} {t('seance.telemetry.units.bytes')}
</span>
</div>
<div className="tel-row">
<span className="tel-key">{t('seance.telemetry.latencyVar')}</span>
<span className="tel-val flicker-soft">
{tel.latency_variance_ms.toFixed(2)} {t('seance.telemetry.units.ms')}
</span>
</div>
<div className="tel-row">
<span className="tel-key">{t('seance.telemetry.latencyMean')}</span>
<span className="tel-val flicker-soft">
{tel.latency_mean_ms.toFixed(1)} {t('seance.telemetry.units.ms')}
</span>
</div>
<div className="tel-row">
<span className="tel-key">{t('seance.telemetry.dns')}</span>
<span className="tel-val flicker-soft">
{tel.dns_ms.toFixed(1)} {t('seance.telemetry.units.ms')}
</span>
</div>
</div>
) : (
<p className="dim flicker">{t('common.loading')}</p>
)}
<p className="wire-count">{t('seance.anomaly.count', { count: state.anomalyCount })}</p> <p className="wire-count">{t('seance.anomaly.count', { count: state.anomalyCount })}</p>
{!state.passive && ( {!state.passive && (
<p className="wire-hint dim"> <p className="wire-hint dim">