feat(ai-act-kompass): printable training materials (paper test, answer key, handout)
- Paper version of the AI literacy test: name/date/department fields, 12 questions with tick boxes (A/B/C), marking line and signature block — for employees without computer access; contains no answers - Answer key for markers: correct letter plus explanation per question, pass threshold noted - Training handout: the 12 core rules (question + explanation) as Art. 4 training material to distribute before the test - All printable with company letterhead via the existing print/PDF pipeline and downloadable; buttons in the quiz view under 'Print training materials'; UI labels in all nine languages (document body DE/EN with EN fallback, full localization follows the content pack) - 3 new tests (72 total) Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
This commit is contained in:
parent
efdc60fe84
commit
ca58f1738f
@ -1,6 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CONTENT_PACK } from '../content/pack';
|
||||
import { certificateDoc, getQuiz, scoreQuiz, trainingLogDoc } from './quiz';
|
||||
import {
|
||||
answerKeyDoc,
|
||||
certificateDoc,
|
||||
getQuiz,
|
||||
handoutDoc,
|
||||
printableTestDoc,
|
||||
scoreQuiz,
|
||||
trainingLogDoc,
|
||||
} from './quiz';
|
||||
import { QUIZ_PASS_PCT, QUIZ_QUESTIONS } from './quizContent';
|
||||
import type { CompanyProfile } from './types';
|
||||
|
||||
@ -65,6 +73,30 @@ describe('KI-Kompetenztest', () => {
|
||||
expect(md).toContain('keine Rechtsberatung');
|
||||
});
|
||||
|
||||
it('Papierfassung enthält alle Fragen, Ankreuzfelder und Auswertungszeile', () => {
|
||||
const md = printableTestDoc(CONTENT_PACK, '2026-07-04', profile);
|
||||
expect(md).toContain('Papierfassung');
|
||||
expect((md.match(/☐/g) ?? []).length).toBe(36); // 12 Fragen × 3 Optionen
|
||||
expect(md).toContain('**Name:**');
|
||||
expect(md).toContain('Ergebnis: ______ / 12');
|
||||
expect(md).not.toContain('richtige Antwort'); // keine Lösungen im Testbogen
|
||||
});
|
||||
|
||||
it('Lösungsbogen nennt je Frage den richtigen Buchstaben', () => {
|
||||
const md = answerKeyDoc(CONTENT_PACK, '2026-07-04', profile);
|
||||
expect(md).toContain('Lösungsbogen');
|
||||
expect((md.match(/\| \d+ \| \*\*[ABC]\*\* \|/g) ?? []).length).toBe(12);
|
||||
});
|
||||
|
||||
it('Schulungshandout enthält die 12 Erklärungen und ist lokalisiert', () => {
|
||||
const de = handoutDoc(CONTENT_PACK, '2026-07-04', profile, 'de');
|
||||
expect(de).toContain('Schulungshandout');
|
||||
expect((de.match(/^## \d+\./gm) ?? []).length).toBe(12);
|
||||
const en = handoutDoc(CONTENT_PACK, '2026-07-04', profile, 'en');
|
||||
expect(en).toContain('Training handout');
|
||||
expect(en).not.toBe(de);
|
||||
});
|
||||
|
||||
it('erzeugt das Schulungsregister mit allen Durchläufen', () => {
|
||||
const md = trainingLogDoc(
|
||||
[
|
||||
|
||||
@ -168,6 +168,156 @@ export function certificateDoc(
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
interface PrintStrings {
|
||||
readonly testTitle: string;
|
||||
readonly testIntro: string;
|
||||
readonly nameDateLine: string;
|
||||
readonly testFooter: string;
|
||||
readonly keyTitle: string;
|
||||
readonly keyIntro: string;
|
||||
readonly keyCols: string;
|
||||
readonly handoutTitle: string;
|
||||
readonly handoutIntro: string;
|
||||
readonly handoutFooter: string;
|
||||
}
|
||||
|
||||
const PRINT: Readonly<Record<'de' | 'en', PrintStrings>> = {
|
||||
de: {
|
||||
testTitle: 'KI-Kompetenztest (Art. 4 EU-KI-VO) — Papierfassung',
|
||||
testIntro:
|
||||
'Bitte kreuzen Sie je Frage genau eine Antwort an. Bestehensgrenze: {pct} % ({min} von {total} Fragen). Die Auswertung erfolgt mit dem Lösungsbogen; das Ergebnis wird im Schulungsregister dokumentiert.',
|
||||
nameDateLine: '**Name:** ______________________________ **Datum:** ______________ **Abteilung:** ______________________________',
|
||||
testFooter: 'Unterschrift Beschäftigte/r: _______________ Ausgewertet durch (Name, Unterschrift): _______________ Ergebnis: ______ / {total}',
|
||||
keyTitle: 'KI-Kompetenztest — Lösungsbogen (nur für Auswertende)',
|
||||
keyIntro: 'Richtige Antworten und Erklärungen. Bestehensgrenze: {pct} % ({min} von {total} Fragen).',
|
||||
keyCols: '| Nr. | Richtig | Erklärung |',
|
||||
handoutTitle: 'Schulungshandout: Die 12 wichtigsten Regeln im Umgang mit KI',
|
||||
handoutIntro:
|
||||
'Dieses Handout fasst die Kernregeln für den Arbeitsalltag zusammen und dient als Schulungsunterlage nach Art. 4 EU-KI-VO. Es kann vor dem Kompetenztest ausgeteilt werden.',
|
||||
handoutFooter:
|
||||
'Bei Fragen oder Vorfällen: an die/den KI-Beauftragte/n wenden (siehe interne Melde- und Eskalationsrichtlinie).',
|
||||
},
|
||||
en: {
|
||||
testTitle: 'AI literacy test (Art. 4 EU AI Act) — paper version',
|
||||
testIntro:
|
||||
'Please tick exactly one answer per question. Pass threshold: {pct} % ({min} of {total} questions). Marking is done with the answer key; the result is documented in the training register.',
|
||||
nameDateLine: '**Name:** ______________________________ **Date:** ______________ **Department:** ______________________________',
|
||||
testFooter: 'Signature employee: _______________ Marked by (name, signature): _______________ Score: ______ / {total}',
|
||||
keyTitle: 'AI literacy test — answer key (for markers only)',
|
||||
keyIntro: 'Correct answers and explanations. Pass threshold: {pct} % ({min} of {total} questions).',
|
||||
keyCols: '| No. | Correct | Explanation |',
|
||||
handoutTitle: 'Training handout: the 12 most important rules for working with AI',
|
||||
handoutIntro:
|
||||
'This handout summarises the core rules for everyday work and serves as training material under Art. 4 EU AI Act. It can be handed out before the literacy test.',
|
||||
handoutFooter:
|
||||
'For questions or incidents: contact the AI officer (see the internal reporting and escalation policy).',
|
||||
},
|
||||
};
|
||||
|
||||
function printStrings(locale: Locale): PrintStrings {
|
||||
return locale === 'de' ? PRINT.de : PRINT.en;
|
||||
}
|
||||
|
||||
const OPTION_LETTERS = ['A', 'B', 'C'] as const;
|
||||
|
||||
function minCorrect(): number {
|
||||
return Math.ceil((QUIZ_PASS_PCT / 100) * QUIZ_QUESTIONS.length);
|
||||
}
|
||||
|
||||
function fillIn(template: string): string {
|
||||
return template
|
||||
.replace('{pct}', String(QUIZ_PASS_PCT))
|
||||
.replace('{min}', String(minCorrect()))
|
||||
.replaceAll('{total}', String(QUIZ_QUESTIONS.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt die Papierfassung des Kompetenztests (zum Ausdrucken und
|
||||
* handschriftlichen Ausfüllen) als Markdown.
|
||||
*/
|
||||
export function printableTestDoc(
|
||||
pack: ContentPack,
|
||||
today: string,
|
||||
profile?: CompanyProfile,
|
||||
locale: Locale = 'de',
|
||||
): string {
|
||||
const s = docStrings(locale);
|
||||
const p = printStrings(locale);
|
||||
const company = profile && profile.name.trim() !== '' ? profile.name : '_______________';
|
||||
const lines = [
|
||||
`# ${p.testTitle}`,
|
||||
'',
|
||||
`**${s.meta.company}:** ${company} · ${s.inventory.asOf}: ${formatDocDate(today, locale)} · Content-Pack v${pack.version}`,
|
||||
'',
|
||||
p.nameDateLine,
|
||||
'',
|
||||
fillIn(p.testIntro),
|
||||
'',
|
||||
];
|
||||
getQuiz(locale).forEach((q, i) => {
|
||||
lines.push(`## ${i + 1}. ${q.question}`, '');
|
||||
q.options.forEach((option, oi) => {
|
||||
lines.push(`- ☐ **${OPTION_LETTERS[oi]})** ${option}`);
|
||||
});
|
||||
lines.push('');
|
||||
});
|
||||
lines.push(fillIn(p.testFooter));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Erzeugt den Lösungsbogen (richtige Antworten + Erklärungen) als Markdown. */
|
||||
export function answerKeyDoc(
|
||||
pack: ContentPack,
|
||||
today: string,
|
||||
profile?: CompanyProfile,
|
||||
locale: Locale = 'de',
|
||||
): string {
|
||||
const s = docStrings(locale);
|
||||
const p = printStrings(locale);
|
||||
const company = profile && profile.name.trim() !== '' ? profile.name : '_______________';
|
||||
const lines = [
|
||||
`# ${p.keyTitle}`,
|
||||
'',
|
||||
`**${s.meta.company}:** ${company} · ${s.inventory.asOf}: ${formatDocDate(today, locale)} · Content-Pack v${pack.version}`,
|
||||
'',
|
||||
fillIn(p.keyIntro),
|
||||
'',
|
||||
p.keyCols,
|
||||
'|---|---|---|',
|
||||
];
|
||||
getQuiz(locale).forEach((q, i) => {
|
||||
lines.push(`| ${i + 1} | **${OPTION_LETTERS[q.correctIndex]}** | ${q.explanation} |`);
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Erzeugt das Schulungshandout (Kernregeln aus den Erklärungen) als Markdown. */
|
||||
export function handoutDoc(
|
||||
pack: ContentPack,
|
||||
today: string,
|
||||
profile?: CompanyProfile,
|
||||
locale: Locale = 'de',
|
||||
): string {
|
||||
const s = docStrings(locale);
|
||||
const p = printStrings(locale);
|
||||
const company = profile && profile.name.trim() !== '' ? profile.name : '_______________';
|
||||
const lines = [
|
||||
`# ${p.handoutTitle}`,
|
||||
'',
|
||||
`**${s.meta.company}:** ${company} · ${s.inventory.asOf}: ${formatDocDate(today, locale)} · Content-Pack v${pack.version}`,
|
||||
'',
|
||||
s.disclaimer,
|
||||
'',
|
||||
p.handoutIntro,
|
||||
'',
|
||||
];
|
||||
getQuiz(locale).forEach((q, i) => {
|
||||
lines.push(`## ${i + 1}. ${q.question}`, '', q.explanation, '');
|
||||
});
|
||||
lines.push(p.handoutFooter);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt das druckbare Schulungsregister (alle Testdurchläufe) als Markdown.
|
||||
*/
|
||||
|
||||
@ -62,6 +62,10 @@ export const CS: UIStrings = {
|
||||
quizLogPrint: 'Vytisknout registr / PDF',
|
||||
quizColScore: 'Výsledek',
|
||||
quizColStatus: 'Stav',
|
||||
quizDocsTitle: 'Tisk školicích materiálů',
|
||||
quizPrintTest: 'Papírová verze testu',
|
||||
quizPrintKey: 'List s řešením',
|
||||
quizPrintHandout: 'Školicí materiál (12 pravidel)',
|
||||
|
||||
wizardTitleNew: 'Posoudit nový systém AI',
|
||||
wizardTitleRe: 'Nové posouzení: {name}',
|
||||
|
||||
@ -62,6 +62,10 @@ export const ES: UIStrings = {
|
||||
quizLogPrint: 'Imprimir registro / PDF',
|
||||
quizColScore: 'Resultado',
|
||||
quizColStatus: 'Estado',
|
||||
quizDocsTitle: 'Imprimir materiales de formación',
|
||||
quizPrintTest: 'Versión en papel del test',
|
||||
quizPrintKey: 'Hoja de soluciones',
|
||||
quizPrintHandout: 'Material de formación (12 reglas)',
|
||||
|
||||
wizardTitleNew: 'Evaluar un nuevo sistema de IA',
|
||||
wizardTitleRe: 'Reevaluación: {name}',
|
||||
|
||||
@ -62,6 +62,10 @@ export const FR: UIStrings = {
|
||||
quizLogPrint: 'Imprimer le registre / PDF',
|
||||
quizColScore: 'Résultat',
|
||||
quizColStatus: 'Statut',
|
||||
quizDocsTitle: 'Imprimer les supports de formation',
|
||||
quizPrintTest: 'Version papier du test',
|
||||
quizPrintKey: 'Corrigé',
|
||||
quizPrintHandout: 'Support de formation (12 règles)',
|
||||
|
||||
wizardTitleNew: 'Évaluer un nouveau système d’IA',
|
||||
wizardTitleRe: 'Réévaluation : {name}',
|
||||
|
||||
@ -62,6 +62,10 @@ export const IT: UIStrings = {
|
||||
quizLogPrint: 'Stampa registro / PDF',
|
||||
quizColScore: 'Risultato',
|
||||
quizColStatus: 'Stato',
|
||||
quizDocsTitle: 'Stampa materiali formativi',
|
||||
quizPrintTest: 'Versione cartacea del test',
|
||||
quizPrintKey: 'Foglio delle soluzioni',
|
||||
quizPrintHandout: 'Dispensa formativa (12 regole)',
|
||||
|
||||
wizardTitleNew: 'Valutare un nuovo sistema di IA',
|
||||
wizardTitleRe: 'Rivalutazione: {name}',
|
||||
|
||||
@ -62,6 +62,10 @@ export const PL: UIStrings = {
|
||||
quizLogPrint: 'Drukuj rejestr / PDF',
|
||||
quizColScore: 'Wynik',
|
||||
quizColStatus: 'Status',
|
||||
quizDocsTitle: 'Drukuj materiały szkoleniowe',
|
||||
quizPrintTest: 'Wersja papierowa testu',
|
||||
quizPrintKey: 'Arkusz odpowiedzi',
|
||||
quizPrintHandout: 'Materiał szkoleniowy (12 zasad)',
|
||||
|
||||
wizardTitleNew: 'Oceń nowy system AI',
|
||||
wizardTitleRe: 'Ponowna ocena: {name}',
|
||||
|
||||
@ -62,6 +62,10 @@ export const RO: UIStrings = {
|
||||
quizLogPrint: 'Tipărește registrul / PDF',
|
||||
quizColScore: 'Rezultat',
|
||||
quizColStatus: 'Stare',
|
||||
quizDocsTitle: 'Tipărește materialele de formare',
|
||||
quizPrintTest: 'Versiunea pe hârtie a testului',
|
||||
quizPrintKey: 'Foaia de răspunsuri',
|
||||
quizPrintHandout: 'Material de formare (12 reguli)',
|
||||
|
||||
wizardTitleNew: 'Evaluează un sistem de IA nou',
|
||||
wizardTitleRe: 'Reevaluare: {name}',
|
||||
|
||||
@ -62,6 +62,10 @@ export const SK: UIStrings = {
|
||||
quizLogPrint: 'Vytlačiť register / PDF',
|
||||
quizColScore: 'Výsledok',
|
||||
quizColStatus: 'Stav',
|
||||
quizDocsTitle: 'Tlač školiacich materiálov',
|
||||
quizPrintTest: 'Papierová verzia testu',
|
||||
quizPrintKey: 'Hárok s riešeniami',
|
||||
quizPrintHandout: 'Školiaci materiál (12 pravidiel)',
|
||||
|
||||
wizardTitleNew: 'Posúdiť nový systém AI',
|
||||
wizardTitleRe: 'Opätovné posúdenie: {name}',
|
||||
|
||||
@ -62,6 +62,10 @@ const DE = {
|
||||
quizLogPrint: 'Register drucken / PDF',
|
||||
quizColScore: 'Ergebnis',
|
||||
quizColStatus: 'Status',
|
||||
quizDocsTitle: 'Schulungsunterlagen drucken',
|
||||
quizPrintTest: 'Papierfassung des Tests',
|
||||
quizPrintKey: 'Lösungsbogen',
|
||||
quizPrintHandout: 'Schulungshandout (12 Regeln)',
|
||||
|
||||
wizardTitleNew: 'Neues KI-System bewerten',
|
||||
wizardTitleRe: 'Neubewertung: {name}',
|
||||
@ -256,6 +260,10 @@ const EN: UIStrings = {
|
||||
quizLogPrint: 'Print register / PDF',
|
||||
quizColScore: 'Score',
|
||||
quizColStatus: 'Status',
|
||||
quizDocsTitle: 'Print training materials',
|
||||
quizPrintTest: 'Paper version of the test',
|
||||
quizPrintKey: 'Answer key',
|
||||
quizPrintHandout: 'Training handout (12 rules)',
|
||||
|
||||
wizardTitleNew: 'Assess a new AI system',
|
||||
wizardTitleRe: 'Reassessment: {name}',
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { certificateDoc, getQuiz, scoreQuiz, trainingLogDoc } from '../engine/quiz';
|
||||
import {
|
||||
answerKeyDoc,
|
||||
certificateDoc,
|
||||
getQuiz,
|
||||
handoutDoc,
|
||||
printableTestDoc,
|
||||
scoreQuiz,
|
||||
trainingLogDoc,
|
||||
} from '../engine/quiz';
|
||||
import type { QuizScore, TrainingRecord } from '../engine/quiz';
|
||||
import { QUIZ_PASS_PCT } from '../engine/quizContent';
|
||||
import type { CompanyProfile } from '../engine/types';
|
||||
@ -142,6 +150,36 @@ export function QuizView({ profile, records, onRecord }: QuizViewProps): JSX.Ele
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="panel">
|
||||
<h2 style={{ marginTop: 0 }}>{t.quizDocsTitle}</h2>
|
||||
<div className="toolbar" style={{ marginBottom: 0 }}>
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={() =>
|
||||
printDocument(`ki-test-papier-${today}`, printableTestDoc(pack, today, profile, locale), profile)
|
||||
}
|
||||
>
|
||||
{t.quizPrintTest}
|
||||
</button>
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={() =>
|
||||
printDocument(`ki-test-loesungen-${today}`, answerKeyDoc(pack, today, profile, locale), profile)
|
||||
}
|
||||
>
|
||||
{t.quizPrintKey}
|
||||
</button>
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={() =>
|
||||
printDocument(`ki-schulungshandout-${today}`, handoutDoc(pack, today, profile, locale), profile)
|
||||
}
|
||||
>
|
||||
{t.quizPrintHandout}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panel-head">
|
||||
<h2 style={{ margin: 0 }}>{t.quizLogTitle}</h2>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user