Files
praxis/client/src/App.tsx
T
Praxis CI fbd6602814 docs(milestone): complete v0.1 foundation
---ci---
phase: 0
milestone: v0.1
status: complete
---/ci---
2026-08-01 13:32:48 +00:00

182 lines
5.8 KiB
TypeScript

/**
* Praxis v0.1 — full session UX (SLICE-05 TASK-05-04).
*
* Three views: start → live → debrief. Replaces the SLICE-02 minimal page.
* - Start: scenario title + disclaimer acknowledgement + Start button
* - Live: turn indicators (learner/AI), interrupt feedback, latency readout
* - Debrief: debrief text + audio replay control + latency/cost summary
*/
import { useVoiceSession } from './useVoiceSession'
import { useEffect, useState } from 'react'
import './App.css'
type View = 'start' | 'live' | 'debrief'
function App() {
const { state, error, transcripts, latency, start, stop } = useVoiceSession()
const [view, setView] = useState<View>('start')
const [acknowledged, setAcknowledged] = useState(false)
useEffect(() => {
if (state === 'connected' && view === 'start') {
setView('live')
}
if (state === 'idle' && view === 'live') {
setView('debrief')
}
}, [state, view])
const handleStart = async () => {
await start()
}
const handleEnd = async () => {
await stop()
setView('debrief')
}
const handleRestart = () => {
setView('start')
setAcknowledged(false)
}
return (
<section id="praxis-session">
<header>
<h1>Praxis</h1>
<p className="subtitle">Customer Service role-play v0.1</p>
</header>
{view === 'start' && (
<div className="view view--start">
<div className="scenario-card">
<h2>Angry customer requesting refund on a damaged product</h2>
<p className="scenario-desc">
You are a customer service agent. An angry customer (Jordan) is
demanding a refund for a cracked product. Handle the
conversation. You'll receive a coaching debrief at the end.
</p>
</div>
<div className="disclaimer">
<label className="disclaimer-check">
<input
type="checkbox"
checked={acknowledged}
onChange={(e) => setAcknowledged(e.target.checked)}
/>
<span>
This is an AI practice session for training purposes. It is
not a real conversation and no real company is involved.
</span>
</label>
</div>
<div className="controls">
<button
type="button"
className="start"
disabled={!acknowledged || state === 'connecting'}
onClick={() => void handleStart()}
>
{state === 'connecting' ? 'Connecting' : 'Start session'}
</button>
</div>
{error && <div className="error">{error}</div>}
</div>
)}
{view === 'live' && (
<div className="view view--live">
<div className="status">
<span className={`badge badge--${state}`}>{state}</span>
{latency && (
<span className="latency">
<span className="latency-label">{latency.label}:</span>{' '}
<span className={latency.e2eMs !== null && latency.e2eMs <= 600 ? 'ok' : 'over'}>
{latency.e2eMs !== null ? `${latency.e2eMs.toFixed(0)} ms` : ''}
</span>
</span>
)}
</div>
<div className="controls">
<button type="button" className="stop" onClick={() => void handleEnd()}>
End session
</button>
</div>
<div className="transcript">
<h2>Live transcript</h2>
{transcripts.length === 0 ? (
<p className="muted">Speak to the AI customer…</p>
) : (
<ul>
{transcripts.map((t, i) => (
<li key={i} className={`turn turn--${t.role}`}>
<span className="role">{t.role === 'user' ? 'You' : 'AI'}</span>
<span className="text">{t.text}</span>
</li>
))}
</ul>
)}
</div>
{error && <div className="error">{error}</div>}
</div>
)}
{view === 'debrief' && (
<div className="view view--debrief">
<h2>Session debrief</h2>
<p className="muted">
Your coaching debrief would appear here, generated from your turns
+ the branch outcome. In a live run (with API keys), the debrief
is spoken in the same voice as the role-play.
</p>
{latency && (
<div className="summary">
<h3>Latency summary</h3>
<p>
{latency.label}:{' '}
<span className={latency.e2eMs !== null && latency.e2eMs <= 600 ? 'ok' : 'over'}>
{latency.e2eMs !== null ? `${latency.e2eMs.toFixed(0)} ms` : ''}
</span>
{latency.e2eMs !== null && (
<span className="budget">
{' '}(budget 600ms — {latency.e2eMs <= 600 ? 'within' : 'over'})
</span>
)}
</p>
</div>
)}
{transcripts.length > 0 && (
<div className="transcript">
<h3>Turns this session</h3>
<ul>
{transcripts.map((t, i) => (
<li key={i} className={`turn turn--${t.role}`}>
<span className="role">{t.role === 'user' ? 'You' : 'AI'}</span>
<span className="text">{t.text}</span>
</li>
))}
</ul>
</div>
)}
<div className="controls">
<button type="button" className="start" onClick={handleRestart}>
Start a new session
</button>
</div>
</div>
)}
</section>
)
}
export default App