Merge gap-d-dead-components: wire up TelemetryReadout and EntityCard
This commit is contained in:
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user