Files
adventure/apps/web/src/lib/teacher/extract-text.ts
Zaine a76ba7bbab
Some checks failed
CI / test (push) Has been cancelled
update
2026-06-28 22:20:13 +01:00

84 lines
2.8 KiB
TypeScript
Executable File

const OBJECT_OBJECT = "[object Object]";
export function isObjectObject(value: string | undefined | null): boolean {
return value?.trim() === OBJECT_OBJECT;
}
/** Extract human-readable text from AI fields that may be strings or nested objects. */
export function extractText(value: unknown): string {
if (value == null) return "";
if (typeof value === "string") {
const trimmed = value.trim();
return isObjectObject(trimmed) ? "" : trimmed;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (Array.isArray(value)) {
return value.map((item) => extractText(item)).filter(Boolean).join("; ");
}
if (typeof value === "object") {
const o = value as Record<string, unknown>;
const parts: string[] = [];
for (const key of ["task", "text", "description", "content", "summary", "goal", "step"]) {
const part = extractText(o[key]);
if (part) parts.push(part);
}
if (Array.isArray(o.steps)) {
const steps = o.steps.map((s) => extractText(s)).filter(Boolean);
if (steps.length) parts.push(steps.join("; "));
}
if (Array.isArray(o.instructions)) {
const instructions = o.instructions.map((s) => extractText(s)).filter(Boolean);
if (instructions.length) parts.push(instructions.join("; "));
}
return parts.join(" — ").trim();
}
return "";
}
export function extractStringList(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map((item) => extractText(item)).filter(Boolean);
}
export function normalizeQuizAnswer(
rawAnswer: unknown,
options: string[]
): { answerIndex: number; answerText: string } {
if (options.length === 0) {
const text = extractText(rawAnswer);
return { answerIndex: 0, answerText: text || "See explanation" };
}
if (typeof rawAnswer === "number" && Number.isFinite(rawAnswer)) {
const idx = Math.min(Math.max(0, rawAnswer), options.length - 1);
return { answerIndex: idx, answerText: options[idx] ?? "" };
}
const asString = extractText(rawAnswer).trim();
if (!asString) return { answerIndex: 0, answerText: options[0] ?? "" };
const asNum = Number(asString);
if (Number.isFinite(asNum) && asNum >= 0 && asNum < options.length) {
return { answerIndex: asNum, answerText: options[asNum] ?? "" };
}
const letterMatch = /^[A-Da-d]$/.exec(asString);
if (letterMatch) {
const idx = letterMatch[0].toUpperCase().charCodeAt(0) - 65;
if (idx >= 0 && idx < options.length) {
return { answerIndex: idx, answerText: options[idx] ?? "" };
}
}
const optionIdx = options.findIndex(
(opt) => opt.toLowerCase() === asString.toLowerCase()
);
if (optionIdx >= 0) {
return { answerIndex: optionIdx, answerText: options[optionIdx] ?? "" };
}
return { answerIndex: 0, answerText: asString };
}