import type { SessionReviewResponse, TeacherSessionReviewStatusResponse, UserPrepostMeasureItem, UserPrepostMeasuresResponse, } from "../../lib/api"; import { displayPiiSafeText } from "../../lib/piiDisplay"; export type PrepostMeasureName = UserPrepostMeasureItem["measure_name"]; export type PrepostTimepoint = UserPrepostMeasureItem["timepoint"]; export type WorksheetReviewStatus = | "pending" | "approved" | "changes_requested" | "rejected"; export type WorksheetDecisionStatus = Exclude< WorksheetReviewStatus, "pending" >; export const PREPOST_PILOT_ID = "phase3-pilot-draft"; export const PREPOST_INSTRUMENT_VERSION = "pilot-prepost-scaffold-2026-06-28"; export const PREPOST_SCORE_MIN = 1; export const PREPOST_SCORE_MAX = 5; export const REVIEW_READY_POLL_INTERVAL_MS = 1500; export const REVIEW_READY_POLL_LIMIT = 60; export const PREPOST_MEASURES: Array<{ key: PrepostMeasureName; label: string; hint: string; }> = [ { key: "self_efficacy", label: "자기효능감", hint: "상담 수행 자신감" }, { key: "skill_proficiency", label: "기술숙련도", hint: "기법 적용 숙련감" }, { key: "training_satisfaction", label: "수련만족도", hint: "훈련 경험 만족도", }, ]; export const PREPOST_TIMEPOINTS: Array<{ key: PrepostTimepoint; label: string; }> = [ { key: "pre", label: "사전" }, { key: "post", label: "사후" }, ]; export function teacherReviewFromResponse( saved: TeacherSessionReviewStatusResponse, ): NonNullable { return { status: saved.status, note: saved.note, reviewerId: saved.reviewer_id ?? null, reviewedAt: saved.reviewed_at ?? null, updatedAt: saved.updated_at ?? null, worksheetStatus: saved.worksheet_status ?? "pending", worksheetNote: saved.worksheet_note ?? "", worksheetReviewedAt: saved.worksheet_reviewed_at ?? null, }; } export function prepostDraftKey( measure: PrepostMeasureName, timepoint: PrepostTimepoint, ) { return `${measure}:${timepoint}`; } export function worksheetReviewStatusLabel(status: WorksheetReviewStatus) { if (status === "approved") return "승인"; if (status === "changes_requested") return "수정요청"; if (status === "rejected") return "반려"; return "검수 대기"; } export function splitPrepostDraftKey( key: string, ): [PrepostMeasureName, PrepostTimepoint] | null { const [measure, timepoint] = key.split(":"); if ( PREPOST_MEASURES.some((item) => item.key === measure) && PREPOST_TIMEPOINTS.some((item) => item.key === timepoint) ) { return [measure as PrepostMeasureName, timepoint as PrepostTimepoint]; } return null; } export function prepostScoreText(score: number) { return Number.isInteger(score) ? String(score) : score.toFixed(1).replace(/\.0$/, ""); } export function prepostDraftFromResponse( response: UserPrepostMeasuresResponse | null, ): Record { const next: Record = {}; for (const item of response?.measures ?? []) { next[prepostDraftKey(item.measure_name, item.timepoint)] = prepostScoreText( item.raw_score, ); } return next; } export function parsePrepostScore(value: string): number | null { const trimmed = value.trim(); if (!trimmed) return null; const parsed = Number(trimmed); if (!Number.isFinite(parsed)) return null; if (parsed < PREPOST_SCORE_MIN || parsed > PREPOST_SCORE_MAX) return null; return parsed; } export function displayReviewSummary(summary: string) { if ( /(?:사유:\s*)?(?:engine_error|session evaluation timeout|timeout|RuntimeError)/i.test( summary, ) ) { return "저장된 축어록은 확인했지만 deep-loop 평가 AI 산출물을 표시하지 못했습니다. AI 평가 재시도가 필요합니다."; } return summary; } export function displayEvaluationRetryError(message: string) { if (!message.trim()) return "AI 평가를 다시 실행하지 못했습니다."; return "AI 평가 재시도를 완료하지 못했습니다. 잠시 뒤 다시 실행해 주세요."; } export function displayTranscriptText(text: string) { return displayPiiSafeText(text); } export function shouldPollReviewReady(data: SessionReviewResponse) { return ( !data.reviewReady && data.degraded && data.supervisorState === "평가 대기" && (data.turns?.length ?? 0) > 0 ); } export function noteAuthorLabel(author: string) { const normalized = author.trim(); if (normalized === "ai") return "AI"; if (normalized === "transcript") return "기록"; return normalized || "교수자"; }