All files / web/src/app/api/realtime/euclid/session route.ts

0% Statements 0/80
0% Branches 0/1
0% Functions 0/1
0% Lines 0/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81                                                                                                                                                                 
/**
 * API route that creates an ephemeral session token for a geometry teacher voice call.
 *
 * POST /api/realtime/euclid/session
 * Body: { propositionId, currentStep, isComplete, playgroundMode, characterId? }
 * Returns: { clientSecret, expiresAt, instructions }
 */

import { NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth/withAuth'
import { createRealtimeSession } from '@/lib/voice/createRealtimeSession'
import { PROP_REGISTRY } from '@/components/toys/euclid/propositions/registry'
import { PLAYGROUND_PROP } from '@/components/toys/euclid/propositions/playground'
import { TOOL_HANG_UP } from '@/components/toys/euclid/agent/tools'
import type { GeometryModeContext } from '@/components/toys/euclid/agent/types'
import { getTeacherConfig } from '@/components/toys/euclid/characters/registry'
import type { AttitudeId } from '@/components/toys/euclid/agent/attitudes/types'

export const POST = withAuth(async (request) => {
  try {
    const body = await request.json()
    const { propositionId, currentStep, isComplete, playgroundMode, characterId, attitudeId } = body

    const prop = propositionId === 0 ? PLAYGROUND_PROP : PROP_REGISTRY[propositionId]
    if (typeof propositionId !== 'number' || !prop) {
      return NextResponse.json({ error: 'Invalid propositionId' }, { status: 400 })
    }

    const apiKey = process.env.LLM_OPENAI_API_KEY || process.env.OPENAI_API_KEY
    if (!apiKey) {
      return NextResponse.json({ error: 'OpenAI API key not configured' }, { status: 503 })
    }

    const config = getTeacherConfig(characterId, attitudeId as AttitudeId | undefined)

    // Build greeting mode context for initial instructions
    const ctx: GeometryModeContext = {
      propositionId,
      propositionTitle: prop.title,
      propositionKind: prop.kind ?? 'construction',
      currentStep: typeof currentStep === 'number' ? currentStep : 0,
      totalSteps: prop.steps.length,
      isComplete: !!isComplete,
      construction: { elements: [], nextLabelIndex: 0, nextColorIndex: 0 },
      proofFacts: [],
      screenshotDataUrl: null,
      playgroundMode: !!playgroundMode,
      steps: prop.steps,
    }

    const instructions = config.modes.greeting.getInstructions(ctx)
    const tools = [TOOL_HANG_UP]

    const result = await createRealtimeSession({
      apiKey,
      voice: config.voice.id,
      instructions,
      tools,
    })

    return NextResponse.json({
      clientSecret: result.clientSecret,
      expiresAt: result.expiresAt,
      instructions,
    })
  } catch (error) {
    console.error('[realtime/euclid/session] Error:', error)

    // Pass through classified errors from createRealtimeSession
    if (error instanceof Error && 'code' in error) {
      const classified = error as Error & { code: string; status: number }
      return NextResponse.json(
        { error: classified.message, code: classified.code },
        { status: 502 }
      )
    }

    return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
  }
})