vignette/apps/web/src/pages/session-review/model.ts
2026-08-09 21:04:29 +09:00

154 lines
4.5 KiB
TypeScript

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<SessionReviewResponse["teacherReview"]> {
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<string, string> {
const next: Record<string, string> = {};
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 || "교수자";
}