Files
encoach_frontend/src/utils/exam.differences.ts
2025-02-09 21:12:29 +00:00

123 lines
3.8 KiB
TypeScript

import { Exam } from "@/interfaces/exam";
import { diff, Diff } from "deep-diff";
const EXCLUDED_KEYS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private", "access", "requiresApproval"]);
const PATH_LABELS: Record<string, string> = {
parts: "Parts",
exercises: "Exercises",
userSolutions: "User Solutions",
words: "Words",
options: "Options",
prompt: "Prompt",
text: "Text",
audio: "Audio",
script: "Script",
difficulty: "Difficulty",
shuffle: "Shuffle",
solutions: "Solutions",
variant: "Variant",
prefix: "Prefix",
suffix: "Suffix",
topic: "Topic",
};
export function generateExamDifferences(oldExam: Exam, newExam: Exam): string[] {
const differences = diff(oldExam, newExam) || [];
return differences.map((change) => formatDifference(change)).filter(Boolean) as string[];
}
function formatDifference(change: Diff<any, any>): string | undefined {
if (!change.path) {
return;
}
if (change.path.some((segment) => EXCLUDED_KEYS.has(segment))) {
return;
}
const pathString = pathToHumanReadable(change.path);
switch (change.kind) {
case "N": // New property/element
return `• Added ${pathString} with value: ${formatValue(change.rhs)}\n`;
case "D": // Deleted property/element
return `• Removed ${pathString} which had value: ${formatValue(change.lhs)}\n`;
case "E": // Edited property/element
return `• Changed ${pathString} from ${formatValue(change.lhs)} to ${formatValue(change.rhs)}\n`;
case "A": // Array change
return formatArrayChange(change);
default:
return;
}
}
function formatArrayChange(change: Diff<any, any>): string | undefined {
if (!change.path) return;
if (change.path.some((segment) => EXCLUDED_KEYS.has(segment))) {
return;
}
const pathString = pathToHumanReadable(change.path);
const arrayChange = (change as any).item;
const idx = (change as any).index;
if (!arrayChange) return;
switch (arrayChange.kind) {
case "N":
return `• Added an item at [#${idx + 1}] in ${pathString}: ${formatValue(arrayChange.rhs)}\n`;
case "D":
return `• Removed an item at [#${idx + 1}] in ${pathString}: ${formatValue(arrayChange.lhs)}\n`;
case "E":
return `• Edited an item at [#${idx + 1}] in ${pathString} from ${formatValue(arrayChange.lhs)} to ${formatValue(arrayChange.rhs)}\n`;
case "A":
return `• Complex array change at [#${idx + 1}] in ${pathString}: ${JSON.stringify(arrayChange)}\n`;
default:
return;
}
}
function formatValue(value: any): string {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "object") {
try {
return JSON.stringify(
value,
(key, val) => {
if (EXCLUDED_KEYS.has(key)) {
return undefined;
}
return val;
},
2 // optional indentation for readability
);
} catch {
return String(value);
}
}
return JSON.stringify(value);
}
/**
* Convert an array of path segments into a friendlier string.
* Example:
* ["parts", 0, "exercises", 1, "prompt"]
* becomes:
* "Parts → [#1] → Exercises → [#2] → Prompt"
*/
function pathToHumanReadable(pathSegments: Array<string | number>): string {
return pathSegments
.map((seg) => {
if (typeof seg === "number") {
return `[#${seg + 1}]`;
}
return PATH_LABELS[seg] ?? seg;
})
.join(" → ");
}