mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
fixed feedback, feedback only supported in descriptions, not in questions for now
This commit is contained in:
@@ -1,13 +1,5 @@
|
||||
type FeedbackType = "+" | "-" | "...";
|
||||
|
||||
const isFeedbackStart = (
|
||||
trimmedLine: string,
|
||||
feedbackType: FeedbackType
|
||||
): boolean => {
|
||||
const prefix = feedbackType === "..." ? "... " : `${feedbackType} `;
|
||||
return trimmedLine.startsWith(prefix) || trimmedLine === feedbackType;
|
||||
};
|
||||
|
||||
const extractFeedbackContent = (
|
||||
trimmedLine: string,
|
||||
feedbackType: FeedbackType
|
||||
@@ -39,69 +31,65 @@ const saveFeedback = (
|
||||
}
|
||||
};
|
||||
|
||||
type feedbackTypeOptions = "correct" | "incorrect" | "neutral" | "none";
|
||||
|
||||
export const quizFeedbackMarkdownUtils = {
|
||||
extractFeedback(
|
||||
linesWithoutPoints: string[],
|
||||
isAnswerLine: (trimmedLine: string) => boolean
|
||||
): {
|
||||
extractFeedback(lines: string[]): {
|
||||
correctComments?: string;
|
||||
incorrectComments?: string;
|
||||
neutralComments?: string;
|
||||
linesWithoutFeedback: string[];
|
||||
otherLines: string[];
|
||||
} {
|
||||
const comments: {
|
||||
correct?: string;
|
||||
incorrect?: string;
|
||||
neutral?: string;
|
||||
} = {};
|
||||
const linesWithoutFeedback: string[] = [];
|
||||
const comments = {
|
||||
correct: [] as string[],
|
||||
incorrect: [] as string[],
|
||||
neutral: [] as string[],
|
||||
};
|
||||
|
||||
let currentFeedbackType: FeedbackType | null = null;
|
||||
let currentFeedbackLines: string[] = [];
|
||||
const otherLines: string[] = [];
|
||||
|
||||
for (const line of linesWithoutPoints) {
|
||||
const trimmed = line.trim();
|
||||
const feedbackIndicators = {
|
||||
correct: "+",
|
||||
incorrect: "-",
|
||||
neutral: "...",
|
||||
};
|
||||
|
||||
// Check if this is a new feedback line
|
||||
let newFeedbackType: FeedbackType | null = null;
|
||||
if (isFeedbackStart(trimmed, "+")) {
|
||||
newFeedbackType = "+";
|
||||
} else if (isFeedbackStart(trimmed, "-")) {
|
||||
newFeedbackType = "-";
|
||||
} else if (isFeedbackStart(trimmed, "...")) {
|
||||
newFeedbackType = "...";
|
||||
}
|
||||
let currentFeedbackType: feedbackTypeOptions = "none";
|
||||
|
||||
if (newFeedbackType) {
|
||||
// Save previous feedback if any
|
||||
saveFeedback(currentFeedbackType, currentFeedbackLines, comments);
|
||||
for (const line of lines.map((l) => l)) {
|
||||
const lineFeedbackType: feedbackTypeOptions = line.startsWith("+")
|
||||
? "correct"
|
||||
: line.startsWith("-")
|
||||
? "incorrect"
|
||||
: line.startsWith("...")
|
||||
? "neutral"
|
||||
: "none";
|
||||
|
||||
// Start new feedback
|
||||
currentFeedbackType = newFeedbackType;
|
||||
const content = extractFeedbackContent(trimmed, newFeedbackType);
|
||||
currentFeedbackLines = content ? [content] : [];
|
||||
} else if (currentFeedbackType && !isAnswerLine(trimmed)) {
|
||||
// This is a continuation of the current feedback
|
||||
currentFeedbackLines.push(line);
|
||||
if (lineFeedbackType === "none" && currentFeedbackType !== "none") {
|
||||
const lineWithoutIndicator = line
|
||||
.replace(feedbackIndicators[currentFeedbackType], "")
|
||||
.trim();
|
||||
comments[currentFeedbackType].push(lineWithoutIndicator);
|
||||
} else if (lineFeedbackType !== "none") {
|
||||
const lineWithoutIndicator = line
|
||||
.replace(feedbackIndicators[lineFeedbackType], "")
|
||||
.trim();
|
||||
currentFeedbackType = lineFeedbackType;
|
||||
comments[lineFeedbackType].push(lineWithoutIndicator);
|
||||
} else {
|
||||
// Save any pending feedback
|
||||
saveFeedback(currentFeedbackType, currentFeedbackLines, comments);
|
||||
currentFeedbackType = null;
|
||||
currentFeedbackLines = [];
|
||||
|
||||
// This is a regular line
|
||||
linesWithoutFeedback.push(line);
|
||||
otherLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save any remaining feedback
|
||||
saveFeedback(currentFeedbackType, currentFeedbackLines, comments);
|
||||
const correctComments = comments.correct.filter((l) => l).join("\n");
|
||||
const incorrectComments = comments.incorrect.filter((l) => l).join("\n");
|
||||
const neutralComments = comments.neutral.filter((l) => l).join("\n");
|
||||
|
||||
return {
|
||||
correctComments: comments.correct,
|
||||
incorrectComments: comments.incorrect,
|
||||
neutralComments: comments.neutral,
|
||||
linesWithoutFeedback,
|
||||
correctComments: correctComments || undefined,
|
||||
incorrectComments: incorrectComments || undefined,
|
||||
neutralComments: neutralComments || undefined,
|
||||
otherLines,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ const parseMatchingAnswer = (input: string) => {
|
||||
};
|
||||
|
||||
export const quizQuestionAnswerMarkdownUtils = {
|
||||
// getHtmlText(): string {
|
||||
// return MarkdownService.render(this.text);
|
||||
// }
|
||||
|
||||
parseMarkdown(input: string, questionType: string): LocalQuizQuestionAnswer {
|
||||
const isCorrect = input.startsWith("*") || input[1] === "*";
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { QuestionType, LocalQuizQuestion } from "../localQuizQuestion";
|
||||
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
||||
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
|
||||
|
||||
const _validFirstAnswerDelimiters = [
|
||||
"*a)",
|
||||
"a)",
|
||||
"*)",
|
||||
")",
|
||||
"[ ]",
|
||||
"[]",
|
||||
"[*]",
|
||||
"^",
|
||||
];
|
||||
const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"];
|
||||
const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"];
|
||||
|
||||
export const isAnswerLine = (trimmedLine: string): boolean => {
|
||||
return _validFirstAnswerDelimiters.some((prefix) =>
|
||||
trimmedLine.startsWith(prefix)
|
||||
);
|
||||
};
|
||||
|
||||
export const getAnswerStringsWithMultilineSupport = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number
|
||||
) => {
|
||||
const indexOfAnswerStart = linesWithoutPoints.findIndex((l) =>
|
||||
_validFirstAnswerDelimiters.some((prefix) =>
|
||||
l.trimStart().startsWith(prefix)
|
||||
)
|
||||
);
|
||||
if (indexOfAnswerStart === -1) {
|
||||
const debugLine = linesWithoutPoints.find((l) => l.trim().length > 0);
|
||||
throw Error(
|
||||
`question ${
|
||||
questionIndex + 1
|
||||
}: no answers when detecting question type on ${debugLine}`
|
||||
);
|
||||
}
|
||||
|
||||
const answerLinesRaw = linesWithoutPoints.slice(indexOfAnswerStart);
|
||||
|
||||
const answerStartPattern = /^(\*?[a-z]?\))|(?<!\S)\[\s*\]|\[\*\]|\^/;
|
||||
const answerLines = answerLinesRaw.reduce((acc: string[], line: string) => {
|
||||
const isNewAnswer = answerStartPattern.test(line);
|
||||
if (isNewAnswer) {
|
||||
acc.push(line);
|
||||
} else if (acc.length !== 0) {
|
||||
acc[acc.length - 1] += "\n" + line;
|
||||
} else {
|
||||
acc.push(line);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return answerLines;
|
||||
};
|
||||
|
||||
export const getQuestionType = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number
|
||||
): QuestionType => {
|
||||
if (linesWithoutPoints.length === 0) return QuestionType.NONE;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() === "essay"
|
||||
)
|
||||
return QuestionType.ESSAY;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||
"short answer"
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||
"short_answer"
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() ===
|
||||
"short_answer="
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
|
||||
|
||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||
linesWithoutPoints,
|
||||
questionIndex
|
||||
);
|
||||
const firstAnswerLine = answerLines[0];
|
||||
const isMultipleChoice = _multipleChoicePrefix.some((prefix) =>
|
||||
firstAnswerLine.startsWith(prefix)
|
||||
);
|
||||
|
||||
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
|
||||
|
||||
const isMultipleAnswer = _multipleAnswerPrefix.some((prefix) =>
|
||||
firstAnswerLine.startsWith(prefix)
|
||||
);
|
||||
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
|
||||
|
||||
const isMatching = firstAnswerLine.startsWith("^");
|
||||
if (isMatching) return QuestionType.MATCHING;
|
||||
|
||||
return QuestionType.NONE;
|
||||
};
|
||||
|
||||
export const getAnswers = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number,
|
||||
questionType: string
|
||||
): LocalQuizQuestionAnswer[] => {
|
||||
if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
||||
linesWithoutPoints = linesWithoutPoints.slice(
|
||||
0,
|
||||
linesWithoutPoints.length - 1
|
||||
);
|
||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||
linesWithoutPoints,
|
||||
questionIndex
|
||||
);
|
||||
|
||||
const answers = answerLines.map((a) =>
|
||||
quizQuestionAnswerMarkdownUtils.parseMarkdown(a, questionType)
|
||||
);
|
||||
return answers;
|
||||
};
|
||||
|
||||
export const getAnswerMarkdown = (
|
||||
question: LocalQuizQuestion,
|
||||
answer: LocalQuizQuestionAnswer,
|
||||
index: number
|
||||
): string => {
|
||||
const multilineMarkdownCompatibleText = answer.text.startsWith("```")
|
||||
? "\n" + answer.text
|
||||
: answer.text;
|
||||
|
||||
if (question.questionType === "multiple_answers") {
|
||||
const correctIndicator = answer.correct ? "*" : " ";
|
||||
const questionTypeIndicator = `[${correctIndicator}] `;
|
||||
|
||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||
} else if (question.questionType === "matching") {
|
||||
return `^ ${answer.text} - ${answer.matchedText}`;
|
||||
} else {
|
||||
const questionLetter = String.fromCharCode(97 + index);
|
||||
const correctIndicator = answer.correct ? "*" : "";
|
||||
const questionTypeIndicator = `${correctIndicator}${questionLetter}) `;
|
||||
|
||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||
}
|
||||
};
|
||||
@@ -1,153 +1,11 @@
|
||||
import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
|
||||
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
||||
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
|
||||
import { quizFeedbackMarkdownUtils } from "./quizFeedbackMarkdownUtils";
|
||||
|
||||
const _validFirstAnswerDelimiters = [
|
||||
"*a)",
|
||||
"a)",
|
||||
"*)",
|
||||
")",
|
||||
"[ ]",
|
||||
"[]",
|
||||
"[*]",
|
||||
"^",
|
||||
];
|
||||
const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"];
|
||||
const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"];
|
||||
|
||||
const isAnswerLine = (trimmedLine: string): boolean => {
|
||||
return _validFirstAnswerDelimiters.some((prefix) =>
|
||||
trimmedLine.startsWith(prefix)
|
||||
);
|
||||
};
|
||||
|
||||
const getAnswerStringsWithMultilineSupport = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number
|
||||
) => {
|
||||
const indexOfAnswerStart = linesWithoutPoints.findIndex((l) =>
|
||||
_validFirstAnswerDelimiters.some((prefix) =>
|
||||
l.trimStart().startsWith(prefix)
|
||||
)
|
||||
);
|
||||
if (indexOfAnswerStart === -1) {
|
||||
const debugLine = linesWithoutPoints.find((l) => l.trim().length > 0);
|
||||
throw Error(
|
||||
`question ${
|
||||
questionIndex + 1
|
||||
}: no answers when detecting question type on ${debugLine}`
|
||||
);
|
||||
}
|
||||
|
||||
const answerLinesRaw = linesWithoutPoints.slice(indexOfAnswerStart);
|
||||
|
||||
const answerStartPattern = /^(\*?[a-z]?\))|(?<!\S)\[\s*\]|\[\*\]|\^/;
|
||||
const answerLines = answerLinesRaw.reduce((acc: string[], line: string) => {
|
||||
const isNewAnswer = answerStartPattern.test(line);
|
||||
if (isNewAnswer) {
|
||||
acc.push(line);
|
||||
} else if (acc.length !== 0) {
|
||||
acc[acc.length - 1] += "\n" + line;
|
||||
} else {
|
||||
acc.push(line);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return answerLines;
|
||||
};
|
||||
const getQuestionType = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number
|
||||
): QuestionType => {
|
||||
if (linesWithoutPoints.length === 0) return QuestionType.NONE;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() === "essay"
|
||||
)
|
||||
return QuestionType.ESSAY;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||
"short answer"
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||
"short_answer"
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER;
|
||||
if (
|
||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() ===
|
||||
"short_answer="
|
||||
)
|
||||
return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
|
||||
|
||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||
linesWithoutPoints,
|
||||
questionIndex
|
||||
);
|
||||
const firstAnswerLine = answerLines[0];
|
||||
const isMultipleChoice = _multipleChoicePrefix.some((prefix) =>
|
||||
firstAnswerLine.startsWith(prefix)
|
||||
);
|
||||
|
||||
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
|
||||
|
||||
const isMultipleAnswer = _multipleAnswerPrefix.some((prefix) =>
|
||||
firstAnswerLine.startsWith(prefix)
|
||||
);
|
||||
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
|
||||
|
||||
const isMatching = firstAnswerLine.startsWith("^");
|
||||
if (isMatching) return QuestionType.MATCHING;
|
||||
|
||||
return QuestionType.NONE;
|
||||
};
|
||||
|
||||
const getAnswers = (
|
||||
linesWithoutPoints: string[],
|
||||
questionIndex: number,
|
||||
questionType: string
|
||||
): LocalQuizQuestionAnswer[] => {
|
||||
if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
||||
linesWithoutPoints = linesWithoutPoints.slice(
|
||||
0,
|
||||
linesWithoutPoints.length - 1
|
||||
);
|
||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||
linesWithoutPoints,
|
||||
questionIndex
|
||||
);
|
||||
|
||||
const answers = answerLines.map((a) =>
|
||||
quizQuestionAnswerMarkdownUtils.parseMarkdown(a, questionType)
|
||||
);
|
||||
return answers;
|
||||
};
|
||||
|
||||
const getAnswerMarkdown = (
|
||||
question: LocalQuizQuestion,
|
||||
answer: LocalQuizQuestionAnswer,
|
||||
index: number
|
||||
): string => {
|
||||
const multilineMarkdownCompatibleText = answer.text.startsWith("```")
|
||||
? "\n" + answer.text
|
||||
: answer.text;
|
||||
|
||||
if (question.questionType === "multiple_answers") {
|
||||
const correctIndicator = answer.correct ? "*" : " ";
|
||||
const questionTypeIndicator = `[${correctIndicator}] `;
|
||||
|
||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||
} else if (question.questionType === "matching") {
|
||||
return `^ ${answer.text} - ${answer.matchedText}`;
|
||||
} else {
|
||||
const questionLetter = String.fromCharCode(97 + index);
|
||||
const correctIndicator = answer.correct ? "*" : "";
|
||||
const questionTypeIndicator = `${correctIndicator}${questionLetter}) `;
|
||||
|
||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||
}
|
||||
};
|
||||
import {
|
||||
getAnswerMarkdown,
|
||||
getAnswers,
|
||||
getQuestionType,
|
||||
isAnswerLine,
|
||||
} from "./quizQuestionAnswerParsingUtils";
|
||||
|
||||
export const quizQuestionMarkdownUtils = {
|
||||
toMarkdown(question: LocalQuizQuestion): string {
|
||||
@@ -180,7 +38,9 @@ export const quizQuestionMarkdownUtils = {
|
||||
},
|
||||
|
||||
parseMarkdown(input: string, questionIndex: number): LocalQuizQuestion {
|
||||
const lines = input.trim().split("\n");
|
||||
const lines = input
|
||||
.trim()
|
||||
.split("\n");
|
||||
const firstLineIsPoints = lines[0].toLowerCase().includes("points: ");
|
||||
|
||||
const textHasPoints =
|
||||
@@ -196,25 +56,12 @@ export const quizQuestionMarkdownUtils = {
|
||||
|
||||
const linesWithoutPoints = firstLineIsPoints ? lines.slice(1) : lines;
|
||||
|
||||
// Extract feedback comments first
|
||||
const {
|
||||
correctComments,
|
||||
incorrectComments,
|
||||
neutralComments,
|
||||
linesWithoutFeedback,
|
||||
} = quizFeedbackMarkdownUtils.extractFeedback(
|
||||
linesWithoutPoints,
|
||||
isAnswerLine
|
||||
);
|
||||
|
||||
const { linesWithoutAnswers } = linesWithoutFeedback.reduce(
|
||||
const { linesWithoutAnswers } = linesWithoutPoints.reduce(
|
||||
({ linesWithoutAnswers, taking }, currentLine) => {
|
||||
if (!taking)
|
||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||
|
||||
const lineIsAnswer = _validFirstAnswerDelimiters.some((prefix) =>
|
||||
currentLine.trimStart().startsWith(prefix)
|
||||
);
|
||||
const lineIsAnswer = isAnswerLine(currentLine);
|
||||
if (lineIsAnswer)
|
||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||
|
||||
@@ -225,7 +72,8 @@ export const quizQuestionMarkdownUtils = {
|
||||
},
|
||||
{ linesWithoutAnswers: [] as string[], taking: true }
|
||||
);
|
||||
const questionType = getQuestionType(linesWithoutFeedback, questionIndex);
|
||||
|
||||
const questionType = getQuestionType(lines, questionIndex);
|
||||
|
||||
const questionTypesWithoutAnswers = [
|
||||
"essay",
|
||||
@@ -233,17 +81,20 @@ export const quizQuestionMarkdownUtils = {
|
||||
"short_answer",
|
||||
];
|
||||
|
||||
const descriptionLines = questionTypesWithoutAnswers.includes(
|
||||
questionType.toLowerCase()
|
||||
)
|
||||
const descriptionLines = questionTypesWithoutAnswers.includes(questionType)
|
||||
? linesWithoutAnswers
|
||||
.slice(0, linesWithoutFeedback.length)
|
||||
.slice(0, linesWithoutPoints.length)
|
||||
.filter(
|
||||
(line) => !questionTypesWithoutAnswers.includes(line.toLowerCase())
|
||||
)
|
||||
: linesWithoutAnswers;
|
||||
|
||||
const description = descriptionLines.join("\n");
|
||||
const {
|
||||
correctComments,
|
||||
incorrectComments,
|
||||
neutralComments,
|
||||
otherLines: descriptionWithoutFeedback,
|
||||
} = quizFeedbackMarkdownUtils.extractFeedback(descriptionLines);
|
||||
|
||||
const typesWithAnswers = [
|
||||
"multiple_choice",
|
||||
@@ -252,7 +103,7 @@ export const quizQuestionMarkdownUtils = {
|
||||
"short_answer=",
|
||||
];
|
||||
const answers = typesWithAnswers.includes(questionType)
|
||||
? getAnswers(linesWithoutFeedback, questionIndex, questionType)
|
||||
? getAnswers(lines, questionIndex, questionType)
|
||||
: [];
|
||||
|
||||
const answersWithoutDistractors =
|
||||
@@ -266,7 +117,7 @@ export const quizQuestionMarkdownUtils = {
|
||||
: [];
|
||||
|
||||
const question: LocalQuizQuestion = {
|
||||
text: description,
|
||||
text: descriptionWithoutFeedback.join("\n"),
|
||||
questionType,
|
||||
points,
|
||||
answers: answersWithoutDistractors,
|
||||
|
||||
Reference in New Issue
Block a user