feat: complete Quantumancy web app — full frontend + docs

Frontend (React 18 + TS + Vite):
- Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits
- Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive),
  Ouija planchette board spelling utterances, transcript with TTS replay,
  entity dossier, direct contact streaming, passive/active listening
- Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB
  RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact
- Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete
- State: VeilSocket (reconnect/backoff), seance reducer, auth context
- 72 vitest tests green; served by FastAPI at :7777

Docs: README + as-built plans 3-7
This commit is contained in:
Indiana
2026-07-20 21:11:49 +00:00
parent 1ec9ea5863
commit 6edbbbbc2a
48 changed files with 10494 additions and 168 deletions

View File

@@ -0,0 +1,79 @@
import { useTranslation } from 'react-i18next'
import type { SpiritEntity } from '../lib/types'
import { GhostGlyph } from './GhostGlyph'
export type EntityCardProps = {
entity: SpiritEntity | null
isNew: boolean
}
const RARITY_CLASS: Record<string, string> = {
common: 'rarity-common',
uncommon: 'rarity-uncommon',
rare: 'rarity-rare',
mythic: 'rarity-mythic',
}
/** Occult dossier of the currently-summoned spirit, wax-seal vibe. */
export function EntityCard({ entity, isNew }: EntityCardProps) {
const { t } = useTranslation()
if (!entity) {
return (
<aside className="entity-card empty">
<h3 className="ec-title">{t('seance.entity.title')}</h3>
<div className="ec-none">
<div className="ec-none-sigil">⟁</div>
<p>{t('seance.entity.none')}</p>
<p className="dim">{t('seance.entity.noneHint')}</p>
</div>
</aside>
)
}
return (
<aside className="entity-card">
<div className="ec-wax" aria-hidden="true">
{entity.name.slice(0, 1).toUpperCase()}
</div>
<h3 className="ec-title">{t('seance.entity.title')}</h3>
<div className="ec-head">
<GhostGlyph hue={entity.visual.hue} form={entity.visual.form} size={64} />
<div>
<div className="ec-name glitch-hover">{entity.name}</div>
<div className="ec-epithet">{entity.epithet}</div>
<span className={`rarity-badge ${RARITY_CLASS[entity.rarity] ?? ''}`}>
{t(`seance.entity.rarity.${entity.rarity}`)}
</span>
</div>
</div>
<div className="ec-contact">
{isNew ? t('seance.entity.isNew') : t('seance.entity.known', { count: entity.contact_count })}
</div>
<div className="ec-section">
<h4>{t('seance.entity.persona')}</h4>
<p className="ec-persona">{entity.persona}</p>
</div>
{entity.quotes.length > 0 && (
<div className="ec-section">
<h4>{t('seance.entity.quotes')}</h4>
<ul className="ec-quotes">
{entity.quotes.slice(0, 4).map((q, i) => (
<li key={i}>“{q}”</li>
))}
</ul>
</div>
)}
<div className="ec-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>
)
}