84 lines
2.8 KiB
TypeScript
Executable File
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 };
|
|
}
|