feat(tools, client): add speaking tasks logic (#61906)

Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
DanielRosa74
2025-11-07 16:29:21 -03:00
committed by GitHub
parent e32bad05c3
commit 2432f5e9e4
24 changed files with 1388 additions and 82 deletions
@@ -0,0 +1,55 @@
# --description--
Paragraph 1
```html
code example
```
# --instructions--
Paragraph 0
```html
code example 0
```
# --questions--
## --text--
Question line 1
```js
var x = 'y';
```
## --answers--
Some inline `code`
### --feedback--
That is not correct.
### --audio-id--
answer1-audio
---
Some *italics*
A second answer paragraph.
### --audio-id--
answer2-audio
---
<code> code in </code> code tags
## --video-solution--
3
@@ -688,15 +688,18 @@ exports[`challenge parser > should parse video questions 1`] = `
"answers": [
{
"answer": "<p>Some inline <code>code</code></p>",
"audioId": null,
"feedback": "<p>That is not correct.</p>",
},
{
"answer": "<p>Some <em>italics</em></p>
<p>A second answer paragraph.</p>",
"audioId": null,
"feedback": null,
},
{
"answer": "<p><code> code in </code> code tags</p>",
"audioId": null,
"feedback": null,
},
],
@@ -7,15 +7,18 @@ exports[`add-video-question plugin > should match the video snapshot 1`] = `
"answers": [
{
"answer": "<p>Some inline <code>code</code></p>",
"audioId": null,
"feedback": "<p>That is not correct.</p>",
},
{
"answer": "<p>Some <em>italics</em></p>
<p>A second answer paragraph.</p>",
"audioId": null,
"feedback": null,
},
{
"answer": "<p><code> code in </code> code tags</p>",
"audioId": null,
"feedback": null,
},
],
@@ -58,23 +58,56 @@ function getAnswers(answersNodes) {
return answerGroups.map(answerGroup => {
const answerTree = root(answerGroup);
const feedback = find(answerTree, { value: '--feedback--' });
if (feedback) {
const answerNodes = getAllBefore(answerTree, '--feedback--');
const feedbackNodes = getSection(answerTree, '--feedback--');
const feedbackNodes = getSection(answerTree, '--feedback--');
const audioIdNodes = getSection(answerTree, '--audio-id--');
const hasFeedback = feedbackNodes.length > 0;
const hasAudioId = audioIdNodes.length > 0;
if (hasFeedback || hasAudioId) {
let answerNodes;
if (hasFeedback && hasAudioId) {
const feedbackHeading = find(answerTree, {
type: 'heading',
children: [{ type: 'text', value: '--feedback--' }]
});
const audioIdHeading = find(answerTree, {
type: 'heading',
children: [{ type: 'text', value: '--audio-id--' }]
});
const feedbackIndex = answerTree.children.indexOf(feedbackHeading);
const audioIdIndex = answerTree.children.indexOf(audioIdHeading);
const firstMarker =
feedbackIndex < audioIdIndex ? '--feedback--' : '--audio-id--';
answerNodes = getAllBefore(answerTree, firstMarker);
} else if (hasFeedback) {
answerNodes = getAllBefore(answerTree, '--feedback--');
} else {
answerNodes = getAllBefore(answerTree, '--audio-id--');
}
if (answerNodes.length < 1) {
throw Error('Answer missing');
}
let extractedAudioId = null;
if (hasAudioId) {
const audioIdContent = getParagraphContent(audioIdNodes[0]);
if (audioIdContent && audioIdContent.trim()) {
extractedAudioId = audioIdContent.trim();
}
}
return {
answer: mdastToHtml(answerNodes),
feedback: mdastToHtml(feedbackNodes)
feedback: hasFeedback ? mdastToHtml(feedbackNodes) : null,
audioId: extractedAudioId
};
}
return { answer: mdastToHtml(answerGroup), feedback: null };
return { answer: mdastToHtml(answerGroup), feedback: null, audioId: null };
});
}
@@ -3,7 +3,11 @@ import parseFixture from '../__fixtures__/parse-fixture';
import addVideoQuestion from './add-video-question';
describe('add-video-question plugin', () => {
let simpleAST, videoAST, multipleQuestionAST, videoOutOfOrderAST;
let simpleAST,
videoAST,
multipleQuestionAST,
videoOutOfOrderAST,
videoWithAudioAST;
const plugin = addVideoQuestion();
let file = { data: {} };
@@ -16,6 +20,7 @@ describe('add-video-question plugin', () => {
videoOutOfOrderAST = await parseFixture(
'with-video-question-out-of-order.md'
);
videoWithAudioAST = await parseFixture('with-video-question-audio.md');
});
beforeEach(() => {
@@ -43,6 +48,7 @@ describe('add-video-question plugin', () => {
expect(question.answers[0]).toHaveProperty('answer');
expect(question.answers[0].answer).toBeTruthy();
expect(question.answers[0]).toHaveProperty('feedback');
expect(question.answers[0]).toHaveProperty('audioId');
};
it('should generate a questions array from a video challenge AST', () => {
@@ -76,16 +82,19 @@ describe('add-video-question plugin', () => {
expect(testObject.solution).toBe(3);
expect(testObject.answers[0]).toStrictEqual({
answer: '<p>Some inline <code>code</code></p>',
feedback: '<p>That is not correct.</p>'
feedback: '<p>That is not correct.</p>',
audioId: null
});
expect(testObject.answers[1]).toStrictEqual({
answer: `<p>Some <em>italics</em></p>
<p>A second answer paragraph.</p>`,
feedback: null
feedback: null,
audioId: null
});
expect(testObject.answers[2]).toStrictEqual({
answer: '<p><code> code in </code> code tags</p>',
feedback: null
feedback: null,
audioId: null
});
});
@@ -101,6 +110,31 @@ describe('add-video-question plugin', () => {
expect(() => plugin(simpleAST)).not.toThrow();
});
it('should extract audioId from answers when present', () => {
plugin(videoWithAudioAST, file);
const testObject = file.data.questions[0];
expect(testObject.answers[0]).toStrictEqual({
answer: '<p>Some inline <code>code</code></p>',
feedback: '<p>That is not correct.</p>',
audioId: 'answer1-audio'
});
expect(testObject.answers[1]).toStrictEqual({
answer: `<p>Some <em>italics</em></p>
<p>A second answer paragraph.</p>`,
feedback: null,
audioId: 'answer2-audio'
});
expect(testObject.answers[2]).toStrictEqual({
answer: '<p><code> code in </code> code tags</p>',
feedback: null,
audioId: null
});
});
it('should match the video snapshot', () => {
plugin(videoAST, file);
expect(file.data).toMatchSnapshot();
@@ -35,6 +35,7 @@ const VALID_MARKERS = [
'## --before-user-code--',
// Level 3
'### --audio-id--',
'### --feedback--',
'### --question--',