feat(evaluasi): feedback kegagalan output & key_text (poin 4)

Tambah missingKeyTextKeys(), reason evaluasi, dan pesan siswa saat output mismatch atau key_text belum dipakai.
This commit is contained in:
a2nr 2026-10-27 16:08:24 +07:00
parent c911908316
commit 72d30351e5
4 changed files with 81 additions and 9 deletions

View File

@ -1,4 +1,4 @@
import { checkKeyText, validateNodes } from '$services/exercise';
import { checkKeyText, missingKeyTextKeys, validateNodes } from '$services/exercise';
import type { LessonContent } from '$types/lesson';
import type { CircuitJSApi } from '$types/circuitjs';
@ -52,6 +52,12 @@ export function evaluateCircuitSubmission(
return { pass: allPassed, output: finalOutput };
}
export interface LanguageEvalResult {
isCorrect: boolean;
reason?: 'output-mismatch' | 'keytext-missing';
missingKeys?: string[];
}
export function processLanguageEvaluation(
compileOutput: string,
code: string,
@ -60,7 +66,7 @@ export function processLanguageEvaluation(
cCode: string,
pythonCode: string,
data: LessonContent
): { isCorrect: boolean } {
): LanguageEvalResult {
if (!data.expected_output && !data.expected_output_python) return { isCorrect: true };
const currentCCode = (currentLanguage === 'c') ? code : cCode;
@ -71,6 +77,15 @@ export function processLanguageEvaluation(
? data.expected_output_python
: data.expected_output;
const isCorrect = compileOutput.trim() === expected.trim() && checkKeyText(mergedCode, data.key_text ?? '');
return { isCorrect };
const outputMatches = compileOutput.trim() === (expected ?? '').trim();
if (!outputMatches) {
return { isCorrect: false, reason: 'output-mismatch' };
}
const missingKeys = missingKeyTextKeys(mergedCode, data.key_text ?? '');
if (missingKeys.length > 0) {
return { isCorrect: false, reason: 'keytext-missing', missingKeys };
}
return { isCorrect: true };
}

View File

@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest';
import { checkKeyText, missingKeyTextKeys } from './exercise';
import { processLanguageEvaluation } from './evaluators';
import type { LessonContent } from '$types/lesson';
describe('missingKeyTextKeys', () => {
it('mengembalikan key yang hilang (bukan hanya boolean)', () => {
expect(missingKeyTextKeys('int main() {}', 'int\nfloat\n%.2lf')).toEqual(['float', '%.2lf']);
});
it('kompatibel dengan checkKeyText', () => {
expect(checkKeyText('int x; float y;', 'int\nfloat')).toBe(true);
expect(checkKeyText('int x;', 'int\nfloat')).toBe(false);
});
it('key_text kosong tidak menghasilkan key hilang', () => {
expect(missingKeyTextKeys('apa saja', ' \n')).toEqual([]);
});
});
describe('processLanguageEvaluation feedback', () => {
const data = {
expected_output: 'Halo',
key_text: 'printf\n%.2f',
} as unknown as LessonContent;
it('output beda → reason output-mismatch', () => {
const result = processLanguageEvaluation('Halo dunia', 'printf("%.2f", 1);', 'c', 'c', '', '', data);
expect(result.isCorrect).toBe(false);
expect(result.reason).toBe('output-mismatch');
});
it('output sama tapi key_text kurang → reason keytext-missing + daftar key', () => {
const result = processLanguageEvaluation('Halo', 'printf("Halo");', 'c', 'c', '', '', data);
expect(result.isCorrect).toBe(false);
expect(result.reason).toBe('keytext-missing');
expect(result.missingKeys).toEqual(['%.2f']);
});
it('output sama dan key lengkap → correct tanpa reason', () => {
const result = processLanguageEvaluation('Halo', 'printf("%.2f", 1);', 'c', 'c', '', '', data);
expect(result).toEqual({ isCorrect: true });
});
});

View File

@ -4,11 +4,16 @@
import type { CircuitJSApi } from '$types/circuitjs';
/** Return key_text keywords (one per line) that are missing from the code. */
export function missingKeyTextKeys(code: string, keyText: string): string[] {
if (!keyText.trim()) return [];
const keys = keyText.split('\n').map(k => k.trim()).filter(k => k.length > 0);
return keys.filter(key => !code.includes(key));
}
/** Check if code contains all required key_text keywords (one per line). */
export function checkKeyText(code: string, keyText: string): boolean {
if (!keyText.trim()) return true;
const keys = keyText.split('\n').map(k => k.trim()).filter(k => k.length > 0);
return keys.every(key => code.includes(key));
return missingKeyTextKeys(code, keyText).length === 0;
}
export interface NodeResult {

View File

@ -865,8 +865,8 @@ export class LessonManager {
out.output = res.output;
out.success = true;
if (this.data.expected_output || this.data.expected_output_python) {
const { isCorrect } = processLanguageEvaluation(res.output, code, lang, this.currentLanguage, this.cCode, this.pythonCode, this.data);
if (isCorrect) {
const evaluation = processLanguageEvaluation(res.output, code, lang, this.currentLanguage, this.cCode, this.pythonCode, this.data);
if (evaluation.isCorrect) {
if (lang === 'c') this.cPassed = true;
else if (lang === 'python') this.pythonPassed = true;
if (!this.checkAllPassed()) {
@ -883,6 +883,14 @@ export class LessonManager {
}
setTimeout(() => { this.showCelebration = false; this.activeTab = 'editor'; }, 3000);
}
} else {
out.success = false;
if (evaluation.reason === 'keytext-missing') {
const missing = (evaluation.missingKeys ?? []).map(k => `\`${k.replace(/\n/g, '\\n')}\``).join(', ');
out.output += `\n❌ Output sudah sesuai, tapi kode belum memakai konsep materi. Kata kunci wajib yang belum ada: ${missing}`;
} else if (evaluation.reason === 'output-mismatch') {
out.output += '\n❌ Output belum sesuai yang diharapkan — periksa format keluaran programmu.';
}
}
}
} catch (err: any) {