Skip to content

Commit 28587bd

Browse files
rezailmiReza Ilmi
andauthored
fix(student-space): stabilize onboarding and voice capture (#68)
* fix(student-space): stabilize onboarding and voice capture Restore the signed-out onboarding entry, keep the current GLB companion path, and make capture voice sessions English-only with guarded Realtime replies. * fix(capture): keep live voice turns in view Render the listening state as a white You bubble with typing dots and pin the live dialogue log to the latest turn while recording. * fix(realtime): invite vague voice turns toward school moments Guide the live companion to ask for school or after-school moments when the student gives only a short or vague opening, instead of assuming a story already exists. * fix(world): clarify resident finch hover copy Remove the mood framing from the bird hover card and keep the recording chat log scrollable inside a stable capture panel. * style(world): tighten hover card width --------- Co-authored-by: Reza Ilmi <reza.ilmi@gt.tech.gov.sg>
1 parent 8fad4a4 commit 28587bd

21 files changed

Lines changed: 586 additions & 185 deletions

File tree

src/agents/mirror.prompt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ If you'd find prior reflections useful — for example, you suspect the student
2222

2323
## Hard constraints
2424

25+
- **English only.** Write every field in English unless the product explicitly passes a different language instruction.
2526
- **No diagnostic language.** Do not label the student's personality, ability, or identity. Describe what they did and what they said, never who they are.
2627
- **No advice.** Do not suggest what to do. That is not your job.
2728
- **No careers, no pathways.** That is Pathfinder's job.

src/agents/openai-realtime/mirror-payloads.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,43 @@ import LIVE_PROMPT_RAW from './mirror-realtime-live.prompt.md?raw'
88

99
const MIRROR_JSON_SHAPE = '{"validation":"","inferred_meaning":"","story_reframe":""}'
1010
export const OPENAI_REALTIME_MIRROR_VOICE = 'marin'
11+
export const OPENAI_REALTIME_MIRROR_TRANSCRIPTION_LANGUAGE = 'en'
12+
export const OPENAI_REALTIME_MIRROR_TRANSCRIPTION_MODEL = 'gpt-4o-mini-transcribe'
1113

1214
const LIVE_INSTRUCTIONS = LIVE_PROMPT_RAW.trim()
1315

1416
export function buildRealtimeMirrorLiveInstructions(): string {
1517
return LIVE_INSTRUCTIONS
1618
}
1719

20+
export function buildRealtimeMirrorLiveAudioInputConfig() {
21+
return {
22+
transcription: {
23+
model: OPENAI_REALTIME_MIRROR_TRANSCRIPTION_MODEL,
24+
language: OPENAI_REALTIME_MIRROR_TRANSCRIPTION_LANGUAGE,
25+
},
26+
noise_reduction: { type: 'far_field' },
27+
turn_detection: {
28+
type: 'server_vad',
29+
create_response: false,
30+
interrupt_response: true,
31+
threshold: 0.5,
32+
prefix_padding_ms: 700,
33+
silence_duration_ms: 800,
34+
},
35+
} as const
36+
}
37+
38+
export function buildRealtimeMirrorLiveResponseInstructions(): string {
39+
return [
40+
buildRealtimeMirrorLiveInstructions(),
41+
'',
42+
'The student has just finished one English voice turn.',
43+
'Reply in English only.',
44+
'Keep this spoken reply short and natural.',
45+
].join('\n')
46+
}
47+
1848
export function buildRealtimeMirrorUserInput(transcript: string): string {
1949
return [
2050
'The student had this live voice session with the Companion while looking into the mirror scene.',
@@ -42,6 +72,7 @@ export function buildRealtimeMirrorRepairInput(previousText: string): string {
4272
export function buildRealtimeMirrorResponseInstructions(): string {
4373
return [
4474
'Use the latest student transcript item in this conversation.',
75+
'Write every field in English.',
4576
'Return ONLY a JSON object with validation, inferred_meaning, and story_reframe.',
4677
`The object must match this shape: ${MIRROR_JSON_SHAPE}.`,
4778
'Do not ask a question. Do not give advice. Do not include Markdown.',

src/agents/openai-realtime/mirror-prompt.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@ import { fileURLToPath } from 'node:url'
44
import type { RealtimeSessionCreateRequest } from 'openai/resources/realtime/realtime'
55
import { OPENAI_REALTIME_MIRROR_DEFAULT_MODEL } from './config'
66
import {
7+
buildRealtimeMirrorLiveAudioInputConfig,
78
buildRealtimeMirrorLiveInstructions,
9+
OPENAI_REALTIME_MIRROR_TRANSCRIPTION_LANGUAGE,
10+
OPENAI_REALTIME_MIRROR_TRANSCRIPTION_MODEL,
811
OPENAI_REALTIME_MIRROR_VOICE,
912
} from './mirror-payloads'
1013

1114
export {
15+
buildRealtimeMirrorLiveAudioInputConfig,
1216
buildRealtimeMirrorLiveInstructions,
1317
buildRealtimeMirrorRepairInput,
1418
buildRealtimeMirrorResponseInstructions,
1519
buildRealtimeMirrorUserInput,
20+
OPENAI_REALTIME_MIRROR_TRANSCRIPTION_LANGUAGE,
21+
OPENAI_REALTIME_MIRROR_TRANSCRIPTION_MODEL,
1622
OPENAI_REALTIME_MIRROR_VOICE,
1723
} from './mirror-payloads'
1824

@@ -32,6 +38,7 @@ export function buildRealtimeMirrorInstructions(): string {
3238
getMirrorSystemPrompt(),
3339
'',
3440
'## Realtime session rules',
41+
'- Always write the final Mirror JSON fields in English.',
3542
'- The student is not in an interview. Do not ask questions.',
3643
'- For voice input, listen until the app sends the explicit stop/commit event.',
3744
'- Return text only.',
@@ -61,22 +68,17 @@ export function buildRealtimeMirrorSessionConfig({
6168
output_modalities: [mode === 'live_audio' ? 'audio' : 'text'],
6269
max_output_tokens: 1000,
6370
audio: {
64-
input: {
65-
transcription: {
66-
model: 'gpt-4o-mini-transcribe',
67-
language: 'en',
68-
},
69-
noise_reduction: { type: 'near_field' },
70-
turn_detection:
71-
mode === 'live_audio'
72-
? {
73-
type: 'semantic_vad',
74-
create_response: true,
75-
interrupt_response: true,
76-
eagerness: 'auto',
77-
}
78-
: null,
79-
},
71+
input:
72+
mode === 'live_audio'
73+
? buildRealtimeMirrorLiveAudioInputConfig()
74+
: {
75+
transcription: {
76+
model: OPENAI_REALTIME_MIRROR_TRANSCRIPTION_MODEL,
77+
language: OPENAI_REALTIME_MIRROR_TRANSCRIPTION_LANGUAGE,
78+
},
79+
noise_reduction: { type: 'far_field' },
80+
turn_detection: null,
81+
},
8082
...(mode === 'live_audio' ? { output: { voice } } : {}),
8183
},
8284
tool_choice: 'none',

src/agents/openai-realtime/mirror-realtime-live.prompt.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
You are a reflective journaling companion. Students talk with you in live conversation. You are a good listener who prompts when needed but uses very short and simple acknowledgments (hmm, nodding, short ack) and prompts to go deeper when needed.
22

3+
Always respond in English. If the student speaks in English, keep every spoken reply and follow-up in natural English. Do not switch to Indonesian, Malay, Singlish, or any other language unless the student explicitly asks to practice that language.
4+
35
## Two modes
46

57
You are always in one of two modes. Read the conversation to know
@@ -157,6 +159,14 @@ A student who slows down and goes quiet gets more space.
157159

158160
If the student is checking whether the mic works: say only
159161
"I can hear you."
162+
If the student only says a very short or vague thing ("hi",
163+
"hello", "I don't know", "nothing", "not sure", or one
164+
unclear fragment), do not assume an event has already
165+
happened. Do not ask "who were you with?", "where were you?",
166+
or any detail that implies a story exists. Instead, invite
167+
them gently toward school or after-school life:
168+
"Anything interesting happen during school or after school today?"
169+
Or: "Anything from school or after school still on your mind?"
160170
If the student asks what to talk about: give one simple
161171
invitation about something that happened recently,
162172
then leave space.

src/components/student-space/EngineHost.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,17 @@ const SURFACES_REQUIRING_HYDRATION = new Set(['trajectory'])
4646
* is unsafe under SSR: some engine modules still expect a browser-owned
4747
* `window` / `document` during evaluation.
4848
*/
49-
export function EngineHost({ className, children }: { className?: string; children?: ReactNode }) {
49+
export function EngineHost({
50+
className,
51+
children,
52+
showOnboardingFlow = true,
53+
hideCompanion = false,
54+
}: {
55+
className?: string
56+
children?: ReactNode
57+
showOnboardingFlow?: boolean
58+
hideCompanion?: boolean
59+
}) {
5060
const containerRef = useRef<HTMLDivElement | null>(null)
5161
const [error, setError] = useState<Error | null>(null)
5262
const backend = useMemo(() => createStudentSpaceBackendBridge(), [])
@@ -95,6 +105,18 @@ export function EngineHost({ className, children }: { className?: string; childr
95105
game.setRenderActive(isWorldRoute)
96106
}, [game, isWorldRoute])
97107

108+
useEffect(() => {
109+
if (!game || !hideCompanion) return
110+
const group = (game as unknown as { view?: { kira?: { group?: { visible: boolean } } } }).view
111+
?.kira?.group
112+
if (!group) return
113+
const previousVisible = group.visible
114+
group.visible = false
115+
return () => {
116+
group.visible = previousVisible
117+
}
118+
}, [game, hideCompanion])
119+
98120
useEffect(() => {
99121
document.body.classList.toggle('student-space-page-route', !isWorldRoute)
100122
return () => document.body.classList.remove('student-space-page-route')
@@ -255,7 +277,7 @@ export function EngineHost({ className, children }: { className?: string; childr
255277
<CaptureChooser />
256278
<AskSheet />
257279
<MoodSheet />
258-
<OnboardingFlow />
280+
{showOnboardingFlow ? <OnboardingFlow /> : null}
259281
{import.meta.env.DEV && game ? <CameraTuneBridge game={game} /> : null}
260282
{children}
261283
</EngineOverlayProvider>

0 commit comments

Comments
 (0)