83
apps/web/src/lib/teacher/extract-text.ts
Normal file
83
apps/web/src/lib/teacher/extract-text.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user