154 lines
4.5 KiB
TypeScript
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 || "교수자";
|
|
}
|