All files / web/src/lib/utils attempt-tracking.ts

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

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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150                                                                                                                                                                                                                                                                                                           
/**
 * Utility functions for tracking and displaying multiple video recording attempts
 * per problem (epoch retries and manual redos).
 */

/**
 * Video info from the API response
 */
export interface VideoAttemptInfo {
  problemNumber: number
  partIndex: number
  epochNumber: number
  attemptNumber: number
  isRetry: boolean
  isManualRedo: boolean
  status: string
  durationMs: number | null
  fileSize: number | null
  isCorrect: boolean | null
  startedAt: Date | string | null
  endedAt: Date | string | null
  processingError: string | null
}

/**
 * Get all attempts for a specific problem from a video list
 */
export function getVideoAttemptsForProblem(
  videos: VideoAttemptInfo[],
  problemNumber: number
): VideoAttemptInfo[] {
  return videos
    .filter((v) => v.problemNumber === problemNumber)
    .sort((a, b) => {
      // Sort by epoch, then attempt number
      if (a.epochNumber !== b.epochNumber) return a.epochNumber - b.epochNumber
      return a.attemptNumber - b.attemptNumber
    })
}

/**
 * Get a human-readable label for an attempt
 */
export function getAttemptLabel(video: VideoAttemptInfo): string {
  if (video.isManualRedo) {
    return `Redo #${video.attemptNumber}`
  }
  if (video.isRetry && video.epochNumber > 0) {
    return `Retry (Round ${video.epochNumber})`
  }
  if (video.attemptNumber > 1) {
    return `Attempt ${video.attemptNumber}`
  }
  return 'Initial attempt'
}

/**
 * Get a short label for dropdown display
 */
export function getAttemptShortLabel(video: VideoAttemptInfo): string {
  if (video.isManualRedo) {
    return `Redo ${video.attemptNumber}`
  }
  if (video.isRetry && video.epochNumber > 0) {
    return `Retry ${video.epochNumber}`
  }
  if (video.attemptNumber > 1) {
    return `Attempt ${video.attemptNumber}`
  }
  return 'Initial'
}

/**
 * Get the total number of unique problems that have recordings
 */
export function getUniqueProblemsWithRecordings(videos: VideoAttemptInfo[]): number[] {
  const problemNumbers = new Set(videos.map((v) => v.problemNumber))
  return Array.from(problemNumbers).sort((a, b) => a - b)
}

/**
 * Group videos by problem number
 */
export function groupVideosByProblem(videos: VideoAttemptInfo[]): Map<number, VideoAttemptInfo[]> {
  const grouped = new Map<number, VideoAttemptInfo[]>()

  for (const video of videos) {
    const existing = grouped.get(video.problemNumber) ?? []
    existing.push(video)
    grouped.set(video.problemNumber, existing)
  }

  // Sort each group
  for (const [problemNumber, attempts] of grouped) {
    grouped.set(
      problemNumber,
      attempts.sort((a, b) => {
        if (a.epochNumber !== b.epochNumber) return a.epochNumber - b.epochNumber
        return a.attemptNumber - b.attemptNumber
      })
    )
  }

  return grouped
}

/**
 * Check if a problem has multiple recording attempts
 */
export function hasMultipleAttempts(videos: VideoAttemptInfo[], problemNumber: number): boolean {
  return getVideoAttemptsForProblem(videos, problemNumber).length > 1
}

/**
 * Get the latest (most recent) attempt for a problem
 */
export function getLatestAttempt(
  videos: VideoAttemptInfo[],
  problemNumber: number
): VideoAttemptInfo | null {
  const attempts = getVideoAttemptsForProblem(videos, problemNumber)
  return attempts.length > 0 ? attempts[attempts.length - 1] : null
}

/**
 * Build the API URL for a video with epoch/attempt params
 */
export function buildVideoUrl(
  playerId: string,
  sessionId: string,
  problemNumber: number,
  epochNumber: number,
  attemptNumber: number
): string {
  return `/api/curriculum/${playerId}/sessions/${sessionId}/problems/${problemNumber}/video?epoch=${epochNumber}&attempt=${attemptNumber}`
}

/**
 * Build the API URL for metadata with epoch/attempt params
 */
export function buildMetadataUrl(
  playerId: string,
  sessionId: string,
  problemNumber: number,
  epochNumber: number,
  attemptNumber: number
): string {
  return `/api/curriculum/${playerId}/sessions/${sessionId}/problems/${problemNumber}/metadata?epoch=${epochNumber}&attempt=${attemptNumber}`
}