feat(ai-act-kompass): AI literacy test (Art. 4) with certificate and training register
- Simple 12-question basics quiz ('for dummies'): how AI works,
hallucinations, confidential data, human oversight, chatbot
disclosure (Art. 50), prohibited practices (Art. 5), high-risk
(Annex III), GDPR, internal incident reporting (Art. 73), shadow AI
approval, content marking, the Art. 4 duty itself — 3 options each,
pass threshold 70%
- Evaluation view with per-question explanations (learning effect),
pass/fail stamp badge
- Printable certificate (letterhead) as Art. 4 evidence: name, date,
score, topics covered, signature lines — files into the existing
training plan documentation
- Local training register (localStorage): every run recorded (name,
date, score, status), printable as continuous Art. 4 evidence
register; cleared with all-data delete
- New nav entry; UI strings in all nine languages; question bank
currently DE/EN (translations for the other seven languages follow
in the next commit)
- 6 new tests (69 total)
Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
This commit is contained in:
parent
3a08392669
commit
21946a157f
83
ai-act-kompass/src/engine/quiz.test.ts
Normal file
83
ai-act-kompass/src/engine/quiz.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CONTENT_PACK } from '../content/pack';
|
||||
import { certificateDoc, getQuiz, scoreQuiz, trainingLogDoc } from './quiz';
|
||||
import { QUIZ_PASS_PCT, QUIZ_QUESTIONS } from './quizContent';
|
||||
import type { CompanyProfile } from './types';
|
||||
|
||||
const profile: CompanyProfile = {
|
||||
name: 'Muster GmbH',
|
||||
street: '',
|
||||
zipCity: '',
|
||||
contactPerson: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
website: '',
|
||||
logoDataUrl: '',
|
||||
countries: [],
|
||||
};
|
||||
|
||||
describe('KI-Kompetenztest', () => {
|
||||
it('hat 12 einfache Fragen mit je 3 Optionen und gültigem Lösungsindex', () => {
|
||||
expect(QUIZ_QUESTIONS).toHaveLength(12);
|
||||
for (const q of QUIZ_QUESTIONS) {
|
||||
expect(q.options).toHaveLength(3);
|
||||
expect([0, 1, 2]).toContain(q.correctIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('löst Fragen lokalisiert auf (DE und EN unterscheiden sich)', () => {
|
||||
const de = getQuiz('de');
|
||||
const en = getQuiz('en');
|
||||
expect(de).toHaveLength(en.length);
|
||||
expect(de[0]?.question).not.toBe(en[0]?.question);
|
||||
});
|
||||
|
||||
it('wertet korrekt aus: alle richtig = bestanden, alle falsch = durchgefallen', () => {
|
||||
const perfect = scoreQuiz(QUIZ_QUESTIONS.map((q) => q.correctIndex));
|
||||
expect(perfect.pct).toBe(100);
|
||||
expect(perfect.passed).toBe(true);
|
||||
|
||||
const wrong = scoreQuiz(QUIZ_QUESTIONS.map((q) => ((q.correctIndex + 1) % 3) as 0 | 1 | 2));
|
||||
expect(wrong.correct).toBe(0);
|
||||
expect(wrong.passed).toBe(false);
|
||||
});
|
||||
|
||||
it('wendet die Bestehensgrenze an (knapp darunter = durchgefallen)', () => {
|
||||
// 8 von 12 = 67 % < 70 % → nicht bestanden; 9 von 12 = 75 % → bestanden
|
||||
const eight = scoreQuiz(
|
||||
QUIZ_QUESTIONS.map((q, i) => (i < 8 ? q.correctIndex : ((q.correctIndex + 1) % 3) as 0 | 1 | 2)),
|
||||
);
|
||||
expect(eight.passed).toBe(false);
|
||||
const nine = scoreQuiz(
|
||||
QUIZ_QUESTIONS.map((q, i) => (i < 9 ? q.correctIndex : ((q.correctIndex + 1) % 3) as 0 | 1 | 2)),
|
||||
);
|
||||
expect(nine.passed).toBe(true);
|
||||
expect(QUIZ_PASS_PCT).toBe(70);
|
||||
});
|
||||
|
||||
it('erzeugt ein Zertifikat mit Name, Ergebnis, Art.-4-Bezug und Disclaimer', () => {
|
||||
const score = scoreQuiz(QUIZ_QUESTIONS.map((q) => q.correctIndex));
|
||||
const md = certificateDoc('Max Mustermann', score, CONTENT_PACK, '2026-07-04', profile);
|
||||
expect(md).toContain('Nachweis KI-Kompetenz (Art. 4');
|
||||
expect(md).toContain('Max Mustermann');
|
||||
expect(md).toContain('BESTANDEN');
|
||||
expect(md).toContain('Muster GmbH');
|
||||
expect(md).toContain('keine Rechtsberatung');
|
||||
});
|
||||
|
||||
it('erzeugt das Schulungsregister mit allen Durchläufen', () => {
|
||||
const md = trainingLogDoc(
|
||||
[
|
||||
{ id: '1', name: 'Max Mustermann', date: '2026-07-04', scorePct: 92, passed: true },
|
||||
{ id: '2', name: 'Erika Musterfrau', date: '2026-07-03', scorePct: 58, passed: false },
|
||||
],
|
||||
CONTENT_PACK,
|
||||
'2026-07-04',
|
||||
profile,
|
||||
);
|
||||
expect(md).toContain('Schulungsregister');
|
||||
expect(md).toContain('Max Mustermann');
|
||||
expect(md).toContain('NICHT BESTANDEN');
|
||||
expect(md).toContain('70 %');
|
||||
});
|
||||
});
|
||||
202
ai-act-kompass/src/engine/quiz.ts
Normal file
202
ai-act-kompass/src/engine/quiz.ts
Normal file
@ -0,0 +1,202 @@
|
||||
import { resolveText } from '../i18n/types';
|
||||
import type { Locale } from '../i18n/types';
|
||||
import { docStrings, formatDocDate } from './docStrings';
|
||||
import { QUIZ_PASS_PCT, QUIZ_QUESTIONS } from './quizContent';
|
||||
import type { CompanyProfile, ContentPack } from './types';
|
||||
|
||||
/** Aufgelöste Quizfrage in einer Zielsprache. */
|
||||
export interface QuizQuestion {
|
||||
readonly id: string;
|
||||
readonly question: string;
|
||||
readonly options: readonly [string, string, string];
|
||||
readonly correctIndex: 0 | 1 | 2;
|
||||
readonly explanation: string;
|
||||
}
|
||||
|
||||
/** Testergebnis einer Person. */
|
||||
export interface QuizScore {
|
||||
readonly correct: number;
|
||||
readonly total: number;
|
||||
readonly pct: number;
|
||||
readonly passed: boolean;
|
||||
}
|
||||
|
||||
/** Eintrag im lokalen Schulungsregister (Art.-4-Nachweis). */
|
||||
export interface TrainingRecord {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly date: string;
|
||||
readonly scorePct: number;
|
||||
readonly passed: boolean;
|
||||
}
|
||||
|
||||
/** Liefert den Fragenkatalog in der gewünschten Sprache. */
|
||||
export function getQuiz(locale: Locale): readonly QuizQuestion[] {
|
||||
return QUIZ_QUESTIONS.map((q) => ({
|
||||
id: q.id,
|
||||
question: resolveText(q.question, locale),
|
||||
options: [
|
||||
resolveText(q.options[0], locale),
|
||||
resolveText(q.options[1], locale),
|
||||
resolveText(q.options[2], locale),
|
||||
],
|
||||
correctIndex: q.correctIndex,
|
||||
explanation: resolveText(q.explanation, locale),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wertet abgegebene Antworten aus.
|
||||
*
|
||||
* @param answers - Gewählter Optionsindex je Frage (Reihenfolge des Katalogs)
|
||||
*/
|
||||
export function scoreQuiz(answers: readonly (number | null)[]): QuizScore {
|
||||
const total = QUIZ_QUESTIONS.length;
|
||||
const correct = QUIZ_QUESTIONS.reduce(
|
||||
(acc, q, i) => acc + (answers[i] === q.correctIndex ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
const pct = Math.round((correct / total) * 100);
|
||||
return { correct, total, pct, passed: pct >= QUIZ_PASS_PCT };
|
||||
}
|
||||
|
||||
interface CertStrings {
|
||||
readonly title: string;
|
||||
readonly intro: string;
|
||||
readonly name: string;
|
||||
readonly date: string;
|
||||
readonly result: string;
|
||||
readonly resultPassed: string;
|
||||
readonly resultFailed: string;
|
||||
readonly topics: string;
|
||||
readonly topicList: string;
|
||||
readonly evidence: string;
|
||||
readonly signature: string;
|
||||
readonly logTitle: string;
|
||||
readonly logIntro: string;
|
||||
readonly logCols: string;
|
||||
}
|
||||
|
||||
const CERT: Readonly<Record<'de' | 'en', CertStrings>> = {
|
||||
de: {
|
||||
title: 'Nachweis KI-Kompetenz (Art. 4 EU-KI-VO)',
|
||||
intro:
|
||||
'Die nachfolgend genannte Person hat den KI-Kompetenztest des Unternehmens absolviert. Der Test vermittelt und prüft die Grundlagen für den verantwortungsvollen Umgang mit KI-Systemen.',
|
||||
name: 'Name',
|
||||
date: 'Datum',
|
||||
result: 'Ergebnis',
|
||||
resultPassed: 'BESTANDEN',
|
||||
resultFailed: 'NICHT BESTANDEN — Wiederholung erforderlich',
|
||||
topics: 'Geprüfte Themen',
|
||||
topicList:
|
||||
'Funktionsweise und Grenzen von KI · Umgang mit vertraulichen Daten · Prüfung von KI-Ausgaben (Halluzinationen) · menschliche Aufsicht · Transparenzpflichten (Art. 50) · verbotene Praktiken (Art. 5) · Hochrisiko-KI (Annex III) · DSGVO · interne Meldewege (Art. 73) · Freigabeprozess (Schatten-KI) · Kennzeichnung KI-generierter Inhalte · KI-Kompetenzpflicht (Art. 4)',
|
||||
evidence:
|
||||
'Dieser Nachweis ist Teil der Art.-4-Dokumentation des Unternehmens (Schulungsplan & Nachweis KI-Kompetenz) und wird dort abgelegt.',
|
||||
signature: 'Ort, Datum: _______________ Unterschrift Beschäftigte/r: _______________ Unterschrift Arbeitgeber: _______________',
|
||||
logTitle: 'Schulungsregister KI-Kompetenz (Art. 4 EU-KI-VO)',
|
||||
logIntro: 'Fortlaufender Nachweis der durchgeführten KI-Kompetenztests. Bestehensgrenze: {pct} %.',
|
||||
logCols: '| Datum | Name | Ergebnis | Status |',
|
||||
},
|
||||
en: {
|
||||
title: 'Evidence of AI literacy (Art. 4 EU AI Act)',
|
||||
intro:
|
||||
'The person named below has completed the company’s AI literacy test. The test teaches and verifies the basics of responsible use of AI systems.',
|
||||
name: 'Name',
|
||||
date: 'Date',
|
||||
result: 'Result',
|
||||
resultPassed: 'PASSED',
|
||||
resultFailed: 'NOT PASSED — retake required',
|
||||
topics: 'Topics covered',
|
||||
topicList:
|
||||
'How AI works and its limits · handling confidential data · checking AI output (hallucinations) · human oversight · transparency obligations (Art. 50) · prohibited practices (Art. 5) · high-risk AI (Annex III) · GDPR · internal reporting channels (Art. 73) · approval process (shadow AI) · marking AI-generated content · AI literacy duty (Art. 4)',
|
||||
evidence:
|
||||
'This evidence forms part of the company’s Art. 4 documentation (AI literacy training plan & record) and is filed there.',
|
||||
signature: 'Place, date: _______________ Signature employee: _______________ Signature employer: _______________',
|
||||
logTitle: 'AI literacy training register (Art. 4 EU AI Act)',
|
||||
logIntro: 'Continuous record of completed AI literacy tests. Pass threshold: {pct} %.',
|
||||
logCols: '| Date | Name | Score | Status |',
|
||||
},
|
||||
};
|
||||
|
||||
function certStrings(locale: Locale): CertStrings {
|
||||
return locale === 'de' ? CERT.de : CERT.en;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt das druckbare Zertifikat (Art.-4-Nachweis) als Markdown.
|
||||
*
|
||||
* @param name - Name der geprüften Person
|
||||
* @param score - Testergebnis
|
||||
* @param pack - Content-Pack (Versionsangabe)
|
||||
* @param today - Datum im ISO-Format
|
||||
* @param profile - Firmenprofil (Briefkopf)
|
||||
* @param locale - Dokumentsprache
|
||||
*/
|
||||
export function certificateDoc(
|
||||
name: string,
|
||||
score: QuizScore,
|
||||
pack: ContentPack,
|
||||
today: string,
|
||||
profile?: CompanyProfile,
|
||||
locale: Locale = 'de',
|
||||
): string {
|
||||
const s = docStrings(locale);
|
||||
const c = certStrings(locale);
|
||||
const company = profile && profile.name.trim() !== '' ? profile.name : '_______________';
|
||||
return [
|
||||
`# ${c.title}`,
|
||||
'',
|
||||
`**${s.meta.company}:** ${company} · Content-Pack v${pack.version}`,
|
||||
'',
|
||||
s.disclaimer,
|
||||
'',
|
||||
c.intro,
|
||||
'',
|
||||
`| | |`,
|
||||
`|---|---|`,
|
||||
`| **${c.name}** | ${name} |`,
|
||||
`| **${c.date}** | ${formatDocDate(today, locale)} |`,
|
||||
`| **${c.result}** | ${score.correct}/${score.total} (${score.pct} %) — **${score.passed ? c.resultPassed : c.resultFailed}** |`,
|
||||
'',
|
||||
`## ${c.topics}`,
|
||||
'',
|
||||
c.topicList,
|
||||
'',
|
||||
c.evidence,
|
||||
'',
|
||||
c.signature,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt das druckbare Schulungsregister (alle Testdurchläufe) als Markdown.
|
||||
*/
|
||||
export function trainingLogDoc(
|
||||
records: readonly TrainingRecord[],
|
||||
pack: ContentPack,
|
||||
today: string,
|
||||
profile?: CompanyProfile,
|
||||
locale: Locale = 'de',
|
||||
): string {
|
||||
const s = docStrings(locale);
|
||||
const c = certStrings(locale);
|
||||
const company = profile && profile.name.trim() !== '' ? profile.name : '_______________';
|
||||
const lines = [
|
||||
`# ${c.logTitle}`,
|
||||
'',
|
||||
`**${s.meta.company}:** ${company} · ${s.inventory.asOf}: ${formatDocDate(today, locale)} · Content-Pack v${pack.version}`,
|
||||
'',
|
||||
s.disclaimer,
|
||||
'',
|
||||
c.logIntro.replace('{pct}', String(QUIZ_PASS_PCT)),
|
||||
'',
|
||||
c.logCols,
|
||||
'|---|---|---|---|',
|
||||
];
|
||||
for (const r of records) {
|
||||
lines.push(
|
||||
`| ${formatDocDate(r.date, locale)} | ${r.name} | ${r.scorePct} % | ${r.passed ? c.resultPassed : c.resultFailed} |`,
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
233
ai-act-kompass/src/engine/quizContent.ts
Normal file
233
ai-act-kompass/src/engine/quizContent.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { lt } from '../i18n/types';
|
||||
import type { LText } from '../i18n/types';
|
||||
|
||||
/**
|
||||
* Fragenkatalog des KI-Kompetenztests (Art. 4 EU-KI-VO).
|
||||
*
|
||||
* Bewusst einfach gehalten („für Dummies"): 12 Fragen zu den
|
||||
* Grundlagen, die jede/r Beschäftigte kennen muss — KI-Basics,
|
||||
* Verbote, Transparenz, Datenschutz, interne Prozesse.
|
||||
* Teil des versionierten Content-Packs; Übersetzungen folgen dem
|
||||
* lt()-Schema (DE/EN Pflicht, weitere Sprachen ergänzbar).
|
||||
*/
|
||||
|
||||
/** Frage in Quellform (lokalisiert). */
|
||||
export interface QuizQuestionSrc {
|
||||
readonly id: string;
|
||||
readonly question: LText;
|
||||
/** Genau drei Antwortoptionen. */
|
||||
readonly options: readonly [LText, LText, LText];
|
||||
/** Index der richtigen Option (0–2). */
|
||||
readonly correctIndex: 0 | 1 | 2;
|
||||
readonly explanation: LText;
|
||||
}
|
||||
|
||||
/** Bestehensgrenze in Prozent. */
|
||||
export const QUIZ_PASS_PCT = 70;
|
||||
|
||||
export const QUIZ_QUESTIONS: readonly QuizQuestionSrc[] = [
|
||||
{
|
||||
id: 'q-basics',
|
||||
question: lt(
|
||||
'Was macht ein KI-System — vereinfacht gesagt?',
|
||||
'What does an AI system do — put simply?',
|
||||
),
|
||||
options: [
|
||||
lt('Es folgt immer exakt fest einprogrammierten Regeln.', 'It always follows exactly pre-programmed rules.'),
|
||||
lt('Es leitet aus Daten selbstständig ab, welche Ausgabe passt — und kann dabei auch danebenliegen.', 'It infers from data which output fits — and can also get it wrong.'),
|
||||
lt('Es versteht Inhalte wie ein Mensch und macht deshalb keine Fehler.', 'It understands content like a human and therefore makes no mistakes.'),
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: lt(
|
||||
'KI erzeugt Ausgaben statistisch aus Daten. Sie kann überzeugend klingen und trotzdem falsch liegen.',
|
||||
'AI generates outputs statistically from data. It can sound convincing and still be wrong.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-secrets',
|
||||
question: lt(
|
||||
'Dürfen Sie Kundendaten oder Geschäftsgeheimnisse in einen öffentlichen KI-Chatbot eingeben?',
|
||||
'May you enter customer data or trade secrets into a public AI chatbot?',
|
||||
),
|
||||
options: [
|
||||
lt('Ja, wenn es die Arbeit beschleunigt.', 'Yes, if it speeds up the work.'),
|
||||
lt('Ja, solange ich das Ergebnis danach lösche.', 'Yes, as long as I delete the result afterwards.'),
|
||||
lt('Nein — nur in vom Unternehmen freigegebene KI-Systeme.', 'No — only into AI systems approved by the company.'),
|
||||
],
|
||||
correctIndex: 2,
|
||||
explanation: lt(
|
||||
'Eingaben in fremde Cloud-KI können gespeichert und weiterverwendet werden. Es gilt die interne KI-Richtlinie: nur freigegebene Systeme nutzen.',
|
||||
'Input into third-party cloud AI can be stored and reused. The internal AI policy applies: use approved systems only.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-hallucination',
|
||||
question: lt(
|
||||
'Eine KI liefert Ihnen eine Antwort mit Quellenangaben. Was tun Sie?',
|
||||
'An AI gives you an answer with source references. What do you do?',
|
||||
),
|
||||
options: [
|
||||
lt('Wichtige Fakten und Quellen prüfen, bevor ich sie verwende — KI erfindet manchmal beides.', 'Check important facts and sources before using them — AI sometimes invents both.'),
|
||||
lt('Direkt übernehmen — mit Quellen ist es verlässlich.', 'Use it directly — with sources it is reliable.'),
|
||||
lt('Die KI bitten, zu bestätigen, dass alles stimmt.', 'Ask the AI to confirm everything is correct.'),
|
||||
],
|
||||
correctIndex: 0,
|
||||
explanation: lt(
|
||||
'„Halluzinationen" sind erfundene, aber plausibel klingende Angaben — auch Quellen können erfunden sein. Selbstbestätigung der KI ist kein Beleg.',
|
||||
'“Hallucinations” are invented but plausible-sounding statements — sources can be invented too. AI self-confirmation is no proof.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-oversight',
|
||||
question: lt(
|
||||
'Die KI schlägt vor, einen Bewerber abzulehnen. Wer entscheidet?',
|
||||
'The AI suggests rejecting an applicant. Who decides?',
|
||||
),
|
||||
options: [
|
||||
lt('Die KI — dafür ist sie da.', 'The AI — that is what it is for.'),
|
||||
lt('Ein Mensch prüft den Vorschlag und trifft die Entscheidung.', 'A human reviews the suggestion and takes the decision.'),
|
||||
lt('Niemand — die Entscheidung fällt automatisch nach 48 Stunden.', 'No one — the decision is taken automatically after 48 hours.'),
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: lt(
|
||||
'Bei Entscheidungen mit Wirkung für Menschen ist die KI nur Unterstützung — die Letztentscheidung trifft ein kompetenter Mensch (menschliche Aufsicht).',
|
||||
'For decisions affecting people, AI is support only — the final decision is taken by a competent human (human oversight).',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-transparency',
|
||||
question: lt(
|
||||
'Ihr Unternehmen setzt einen Kunden-Chatbot ein. Was ist Pflicht?',
|
||||
'Your company uses a customer chatbot. What is mandatory?',
|
||||
),
|
||||
options: [
|
||||
lt('Nichts — Hauptsache, er antwortet freundlich.', 'Nothing — as long as it answers politely.'),
|
||||
lt('Kunden müssen erkennen können, dass sie mit einer KI sprechen.', 'Customers must be able to recognise they are talking to an AI.'),
|
||||
lt('Der Chatbot muss sich als Mensch ausgeben, damit Kunden Vertrauen haben.', 'The chatbot must pose as a human so customers trust it.'),
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: lt(
|
||||
'Art. 50 EU-KI-VO: KI-Interaktion muss offengelegt werden; KI-generierte Inhalte sind zu kennzeichnen.',
|
||||
'Art. 50 EU AI Act: AI interaction must be disclosed; AI-generated content must be marked.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-prohibited',
|
||||
question: lt(
|
||||
'Welche dieser KI-Anwendungen ist in der EU verboten?',
|
||||
'Which of these AI applications is prohibited in the EU?',
|
||||
),
|
||||
options: [
|
||||
lt('Rechtschreibkorrektur in E-Mails.', 'Spell-checking in emails.'),
|
||||
lt('Emotionserkennung zur Überwachung der Stimmung am Arbeitsplatz.', 'Emotion recognition to monitor mood at the workplace.'),
|
||||
lt('Automatische Übersetzung von Dokumenten.', 'Automatic translation of documents.'),
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: lt(
|
||||
'Art. 5 EU-KI-VO verbietet u. a. Emotionserkennung am Arbeitsplatz sowie Social Scoring und manipulative Techniken — seit Februar 2025.',
|
||||
'Art. 5 EU AI Act prohibits emotion recognition in the workplace, social scoring and manipulative techniques, among others — since February 2025.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-highrisk',
|
||||
question: lt(
|
||||
'Warum gelten für KI im Bewerber-Screening besonders strenge Regeln?',
|
||||
'Why do particularly strict rules apply to AI in applicant screening?',
|
||||
),
|
||||
options: [
|
||||
lt('Weil sie über Lebenschancen von Menschen mitentscheidet — sie gilt als Hochrisiko-KI.', 'Because it co-decides on people’s life chances — it counts as high-risk AI.'),
|
||||
lt('Weil sie teuer ist.', 'Because it is expensive.'),
|
||||
lt('Es gelten keine besonderen Regeln.', 'No special rules apply.'),
|
||||
],
|
||||
correctIndex: 0,
|
||||
explanation: lt(
|
||||
'Beschäftigungsbezogene KI (Auswahl, Beförderung, Kündigung) steht in Annex III der EU-KI-VO — Hochrisiko mit strengen Pflichten.',
|
||||
'Employment-related AI (selection, promotion, termination) is listed in Annex III of the EU AI Act — high-risk with strict obligations.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-gdpr',
|
||||
question: lt(
|
||||
'KI und personenbezogene Daten — was gilt?',
|
||||
'AI and personal data — what applies?',
|
||||
),
|
||||
options: [
|
||||
lt('Die DSGVO gilt bei KI nicht.', 'The GDPR does not apply to AI.'),
|
||||
lt('Die DSGVO gilt weiter: Rechtsgrundlage nötig, Datensparsamkeit, Betroffenenrechte.', 'The GDPR continues to apply: legal basis required, data minimisation, data subject rights.'),
|
||||
lt('Solange die KI die Daten nur „liest", ist alles erlaubt.', 'As long as the AI only “reads” the data, everything is allowed.'),
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: lt(
|
||||
'KI-Verarbeitung personenbezogener Daten ist Datenverarbeitung im Sinne der DSGVO — mit allen Pflichten, ggf. inkl. Datenschutz-Folgenabschätzung.',
|
||||
'AI processing of personal data is data processing under the GDPR — with all obligations, possibly including a data protection impact assessment.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-incident',
|
||||
question: lt(
|
||||
'Eine KI verhält sich merkwürdig oder diskriminiert offenbar. Was tun Sie?',
|
||||
'An AI behaves strangely or apparently discriminates. What do you do?',
|
||||
),
|
||||
options: [
|
||||
lt('Weiter nutzen und hoffen, dass es sich gibt.', 'Keep using it and hope it goes away.'),
|
||||
lt('Sofort intern melden (KI-Beauftragte/r) — es gibt gesetzliche Meldefristen.', 'Report it internally immediately (AI officer) — there are statutory reporting deadlines.'),
|
||||
lt('Das System heimlich abschalten und niemandem etwas sagen.', 'Secretly switch the system off and tell no one.'),
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: lt(
|
||||
'Für schwerwiegende Vorfälle gelten Meldefristen von bis zu 15 Tagen (Art. 73). Deshalb: sofort intern melden — der Meldeweg steht in der internen Melderichtlinie.',
|
||||
'Serious incidents are subject to reporting deadlines of up to 15 days (Art. 73). Therefore: report internally at once — the channel is in the internal reporting policy.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-shadow',
|
||||
question: lt(
|
||||
'Sie finden ein tolles neues KI-Tool im Internet. Dürfen Sie es für die Arbeit nutzen?',
|
||||
'You find a great new AI tool on the internet. May you use it for work?',
|
||||
),
|
||||
options: [
|
||||
lt('Ja — was nichts kostet, kann man nutzen.', 'Yes — if it is free, you can use it.'),
|
||||
lt('Ja, wenn Kolleginnen und Kollegen es auch benutzen.', 'Yes, if colleagues use it too.'),
|
||||
lt('Erst nach interner Freigabe — sonst entsteht „Schatten-KI" ohne Kontrolle.', 'Only after internal approval — otherwise “shadow AI” arises without control.'),
|
||||
],
|
||||
correctIndex: 2,
|
||||
explanation: lt(
|
||||
'Nicht freigegebene Tools umgehen Datenschutz-, Sicherheits- und KI-Prüfungen. Es gilt der Freigabeprozess (Freigabeantrag KI-System).',
|
||||
'Unapproved tools bypass data protection, security and AI checks. The approval process applies (AI system approval request).',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-content',
|
||||
question: lt(
|
||||
'Sie veröffentlichen einen KI-generierten Text oder ein KI-Bild. Was ist zu beachten?',
|
||||
'You publish an AI-generated text or image. What must you consider?',
|
||||
),
|
||||
options: [
|
||||
lt('Inhalt fachlich prüfen und KI-generierte Inhalte gemäß Vorgaben kennzeichnen.', 'Review the content professionally and mark AI-generated content according to the rules.'),
|
||||
lt('Nichts — was die KI erstellt, gehört automatisch dem Unternehmen und stimmt.', 'Nothing — whatever the AI creates automatically belongs to the company and is correct.'),
|
||||
lt('Nur die Rechtschreibung prüfen.', 'Only check the spelling.'),
|
||||
],
|
||||
correctIndex: 0,
|
||||
explanation: lt(
|
||||
'KI-Inhalte können Fehler, fremde Rechte oder Verzerrungen enthalten; synthetische Inhalte sind zu kennzeichnen (Art. 50) und Deepfakes sichtbar zu markieren.',
|
||||
'AI content can contain errors, third-party rights or bias; synthetic content must be marked (Art. 50) and deepfakes visibly labelled.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'q-art4',
|
||||
question: lt(
|
||||
'Warum machen Sie diesen Test überhaupt?',
|
||||
'Why are you taking this test in the first place?',
|
||||
),
|
||||
options: [
|
||||
lt('Reine Firmen-Schikane.', 'Pure company chicanery.'),
|
||||
lt('Art. 4 EU-KI-VO verpflichtet Unternehmen seit Februar 2025, KI-Kompetenz ihres Personals sicherzustellen — dieser Test ist Teil des Nachweises.', 'Art. 4 EU AI Act has obliged companies since February 2025 to ensure their staff’s AI literacy — this test is part of the evidence.'),
|
||||
lt('Die KI hat es so angeordnet.', 'The AI ordered it.'),
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: lt(
|
||||
'Die KI-Kompetenzpflicht (Art. 4) gilt für alle Anbieter und Betreiber von KI — unabhängig von der Risikoklasse. Geschulte Beschäftigte sind die wichtigste Schutzmaßnahme.',
|
||||
'The AI literacy duty (Art. 4) applies to all providers and deployers of AI — regardless of risk class. Trained staff are the most important safeguard.',
|
||||
),
|
||||
},
|
||||
];
|
||||
@ -44,6 +44,25 @@ export const CS: UIStrings = {
|
||||
calExport: 'Přidat do kalendáře (.ics)',
|
||||
calHint: 'Exportuje všechny otevřené lhůty jako soubor kalendáře s připomínkami (30 a 7 dní předem) — lze importovat do Outlooku, kalendáře Apple i Google, v telefonu i počítači.',
|
||||
|
||||
navQuiz: 'Test gramotnosti v oblasti AI',
|
||||
quizSub: 'Jednoduchý test základů (12 otázek) gramotnosti v oblasti AI podle čl. 4 — s tisknutelným certifikátem jako dokladem a místním registrem školení.',
|
||||
quizName: 'Jméno účastníka/účastnice *',
|
||||
quizNamePh: 'Jméno a příjmení',
|
||||
quizSubmit: 'Vyhodnotit test',
|
||||
quizAnswerAll: 'Zadejte prosím jméno a odpovězte na všechny otázky.',
|
||||
quizPassed: 'Úspěšně složen',
|
||||
quizFailed: 'Nesložen',
|
||||
quizScoreLine: '{correct} z {total} otázek správně ({pct} %) — hranice úspěšnosti {pass} %.',
|
||||
quizWhy: 'Vysvětlení',
|
||||
quizCert: 'Vytisknout certifikát / PDF',
|
||||
quizRetry: 'Opakovat test',
|
||||
quizLogTitle: 'Registr školení (doklad podle čl. 4)',
|
||||
quizLogHint: 'Každý pokus se ukládá lokálně a slouží jako doklad povinnosti gramotnosti v oblasti AI (čl. 4). Registr pravidelně tiskněte a zakládejte do dokumentace školení.',
|
||||
quizLogEmpty: 'Zatím žádné pokusy.',
|
||||
quizLogPrint: 'Vytisknout registr / PDF',
|
||||
quizColScore: 'Výsledek',
|
||||
quizColStatus: 'Stav',
|
||||
|
||||
wizardTitleNew: 'Posoudit nový systém AI',
|
||||
wizardTitleRe: 'Nové posouzení: {name}',
|
||||
wizardSub: 'Odpovězte na otázky — riziková třída se určí automaticky podle systematiky aktu o umělé inteligenci.',
|
||||
|
||||
@ -44,6 +44,25 @@ export const ES: UIStrings = {
|
||||
calExport: 'Añadir al calendario (.ics)',
|
||||
calHint: 'Exporta todos los plazos abiertos como archivo de calendario con recordatorios (30 y 7 días antes) — importable en Outlook, Apple y Google Calendar, en el móvil y el ordenador.',
|
||||
|
||||
navQuiz: 'Test de alfabetización en IA',
|
||||
quizSub: 'Test sencillo de fundamentos (12 preguntas) sobre alfabetización en IA según el art. 4 — con certificado imprimible como prueba y registro local de formación.',
|
||||
quizName: 'Nombre del/de la participante *',
|
||||
quizNamePh: 'Nombre y apellidos',
|
||||
quizSubmit: 'Evaluar el test',
|
||||
quizAnswerAll: 'Introduzca un nombre y responda a todas las preguntas.',
|
||||
quizPassed: 'Aprobado',
|
||||
quizFailed: 'No aprobado',
|
||||
quizScoreLine: '{correct} de {total} preguntas correctas ({pct} %) — umbral de aprobado {pass} %.',
|
||||
quizWhy: 'Explicación',
|
||||
quizCert: 'Imprimir certificado / PDF',
|
||||
quizRetry: 'Repetir el test',
|
||||
quizLogTitle: 'Registro de formación (prueba art. 4)',
|
||||
quizLogHint: 'Cada intento se guarda en local y sirve como prueba de la obligación de alfabetización en IA (art. 4). Imprima el registro periódicamente y archívelo con su documentación de formación.',
|
||||
quizLogEmpty: 'Aún no hay intentos.',
|
||||
quizLogPrint: 'Imprimir registro / PDF',
|
||||
quizColScore: 'Resultado',
|
||||
quizColStatus: 'Estado',
|
||||
|
||||
wizardTitleNew: 'Evaluar un nuevo sistema de IA',
|
||||
wizardTitleRe: 'Reevaluación: {name}',
|
||||
wizardSub: 'Responda a las preguntas — la clase de riesgo se determina automáticamente según la sistemática del Reglamento de IA.',
|
||||
|
||||
@ -44,6 +44,25 @@ export const FR: UIStrings = {
|
||||
calExport: 'Ajouter au calendrier (.ics)',
|
||||
calHint: 'Exporte toutes les échéances ouvertes sous forme de fichier calendrier avec rappels (30 et 7 jours avant) — importable dans Outlook, Apple et Google Agenda, sur téléphone et ordinateur.',
|
||||
|
||||
navQuiz: 'Test de maîtrise de l’IA',
|
||||
quizSub: 'Test simple des fondamentaux (12 questions) sur la maîtrise de l’IA selon l’art. 4 — avec certificat imprimable comme preuve et registre local de formation.',
|
||||
quizName: 'Nom du participant / de la participante *',
|
||||
quizNamePh: 'Prénom et nom',
|
||||
quizSubmit: 'Évaluer le test',
|
||||
quizAnswerAll: 'Veuillez saisir un nom et répondre à toutes les questions.',
|
||||
quizPassed: 'Réussi',
|
||||
quizFailed: 'Non réussi',
|
||||
quizScoreLine: '{correct} questions sur {total} correctes ({pct} %) — seuil de réussite {pass} %.',
|
||||
quizWhy: 'Explication',
|
||||
quizCert: 'Imprimer le certificat / PDF',
|
||||
quizRetry: 'Repasser le test',
|
||||
quizLogTitle: 'Registre de formation (preuve art. 4)',
|
||||
quizLogHint: 'Chaque passage est enregistré en local et sert de preuve de l’obligation de maîtrise de l’IA (art. 4). Imprimez régulièrement le registre et classez-le avec votre documentation de formation.',
|
||||
quizLogEmpty: 'Aucun passage pour l’instant.',
|
||||
quizLogPrint: 'Imprimer le registre / PDF',
|
||||
quizColScore: 'Résultat',
|
||||
quizColStatus: 'Statut',
|
||||
|
||||
wizardTitleNew: 'Évaluer un nouveau système d’IA',
|
||||
wizardTitleRe: 'Réévaluation : {name}',
|
||||
wizardSub: 'Répondez aux questions — la classe de risque est déterminée automatiquement selon la systématique du règlement IA.',
|
||||
|
||||
@ -44,6 +44,25 @@ export const IT: UIStrings = {
|
||||
calExport: 'Aggiungi al calendario (.ics)',
|
||||
calHint: 'Esporta tutte le scadenze aperte come file di calendario con promemoria (30 e 7 giorni prima) — importabile in Outlook, Apple e Google Calendar, su telefono e computer.',
|
||||
|
||||
navQuiz: 'Test di alfabetizzazione IA',
|
||||
quizSub: 'Test semplice sulle basi (12 domande) dell’alfabetizzazione IA ai sensi dell’art. 4 — con certificato stampabile come prova e registro locale della formazione.',
|
||||
quizName: 'Nome del/della partecipante *',
|
||||
quizNamePh: 'Nome e cognome',
|
||||
quizSubmit: 'Valuta il test',
|
||||
quizAnswerAll: 'Inserite un nome e rispondete a tutte le domande.',
|
||||
quizPassed: 'Superato',
|
||||
quizFailed: 'Non superato',
|
||||
quizScoreLine: '{correct} domande su {total} corrette ({pct} %) — soglia di superamento {pass} %.',
|
||||
quizWhy: 'Spiegazione',
|
||||
quizCert: 'Stampa certificato / PDF',
|
||||
quizRetry: 'Ripeti il test',
|
||||
quizLogTitle: 'Registro formazione (prova art. 4)',
|
||||
quizLogHint: 'Ogni esecuzione viene salvata in locale e serve come prova dell’obbligo di alfabetizzazione IA (art. 4). Stampate regolarmente il registro e archiviatelo con la documentazione formativa.',
|
||||
quizLogEmpty: 'Nessuna esecuzione finora.',
|
||||
quizLogPrint: 'Stampa registro / PDF',
|
||||
quizColScore: 'Risultato',
|
||||
quizColStatus: 'Stato',
|
||||
|
||||
wizardTitleNew: 'Valutare un nuovo sistema di IA',
|
||||
wizardTitleRe: 'Rivalutazione: {name}',
|
||||
wizardSub: 'Rispondete alle domande — la classe di rischio viene determinata automaticamente secondo la sistematica del regolamento IA.',
|
||||
|
||||
@ -44,6 +44,25 @@ export const PL: UIStrings = {
|
||||
calExport: 'Dodaj do kalendarza (.ics)',
|
||||
calHint: 'Eksportuje wszystkie otwarte terminy jako plik kalendarza z przypomnieniami (30 i 7 dni wcześniej) — do zaimportowania w Outlooku, kalendarzu Apple i Google, na telefonie i komputerze.',
|
||||
|
||||
navQuiz: 'Test kompetencji AI',
|
||||
quizSub: 'Prosty test podstaw (12 pytań) z kompetencji w zakresie AI zgodnie z art. 4 — z drukowanym certyfikatem jako dowodem i lokalnym rejestrem szkoleń.',
|
||||
quizName: 'Imię i nazwisko uczestnika/uczestniczki *',
|
||||
quizNamePh: 'Imię i nazwisko',
|
||||
quizSubmit: 'Oceń test',
|
||||
quizAnswerAll: 'Proszę podać imię i nazwisko oraz odpowiedzieć na wszystkie pytania.',
|
||||
quizPassed: 'Zaliczony',
|
||||
quizFailed: 'Niezaliczony',
|
||||
quizScoreLine: '{correct} z {total} pytań poprawnie ({pct} %) — próg zaliczenia {pass} %.',
|
||||
quizWhy: 'Wyjaśnienie',
|
||||
quizCert: 'Drukuj certyfikat / PDF',
|
||||
quizRetry: 'Powtórz test',
|
||||
quizLogTitle: 'Rejestr szkoleń (dowód z art. 4)',
|
||||
quizLogHint: 'Każde podejście jest zapisywane lokalnie i służy jako dowód obowiązku kompetencji w zakresie AI (art. 4). Regularnie drukuj rejestr i dołączaj do dokumentacji szkoleniowej.',
|
||||
quizLogEmpty: 'Brak podejść.',
|
||||
quizLogPrint: 'Drukuj rejestr / PDF',
|
||||
quizColScore: 'Wynik',
|
||||
quizColStatus: 'Status',
|
||||
|
||||
wizardTitleNew: 'Oceń nowy system AI',
|
||||
wizardTitleRe: 'Ponowna ocena: {name}',
|
||||
wizardSub: 'Odpowiedz na pytania — klasa ryzyka zostanie ustalona automatycznie zgodnie z systematyką rozporządzenia w sprawie AI.',
|
||||
|
||||
@ -44,6 +44,25 @@ export const RO: UIStrings = {
|
||||
calExport: 'Adaugă în calendar (.ics)',
|
||||
calHint: 'Exportă toate termenele deschise ca fișier de calendar cu mementouri (cu 30 și 7 zile înainte) — importabil în Outlook, Apple și Google Calendar, pe telefon și pe computer.',
|
||||
|
||||
navQuiz: 'Test de alfabetizare în IA',
|
||||
quizSub: 'Test simplu de bază (12 întrebări) privind alfabetizarea în domeniul IA conform art. 4 — cu certificat imprimabil ca dovadă și registru local de formare.',
|
||||
quizName: 'Numele participantului/participantei *',
|
||||
quizNamePh: 'Prenume și nume',
|
||||
quizSubmit: 'Evaluează testul',
|
||||
quizAnswerAll: 'Vă rugăm să introduceți numele și să răspundeți la toate întrebările.',
|
||||
quizPassed: 'Promovat',
|
||||
quizFailed: 'Nepromovat',
|
||||
quizScoreLine: '{correct} din {total} întrebări corecte ({pct} %) — prag de promovare {pass} %.',
|
||||
quizWhy: 'Explicație',
|
||||
quizCert: 'Tipărește certificatul / PDF',
|
||||
quizRetry: 'Repetă testul',
|
||||
quizLogTitle: 'Registrul de formare (dovadă art. 4)',
|
||||
quizLogHint: 'Fiecare încercare este salvată local și servește drept dovadă a obligației de alfabetizare în IA (art. 4). Tipăriți regulat registrul și arhivați-l cu documentația de formare.',
|
||||
quizLogEmpty: 'Nicio încercare până acum.',
|
||||
quizLogPrint: 'Tipărește registrul / PDF',
|
||||
quizColScore: 'Rezultat',
|
||||
quizColStatus: 'Stare',
|
||||
|
||||
wizardTitleNew: 'Evaluează un sistem de IA nou',
|
||||
wizardTitleRe: 'Reevaluare: {name}',
|
||||
wizardSub: 'Răspundeți la întrebări — clasa de risc este determinată automat conform sistematicii Regulamentului privind IA.',
|
||||
|
||||
@ -44,6 +44,25 @@ export const SK: UIStrings = {
|
||||
calExport: 'Pridať do kalendára (.ics)',
|
||||
calHint: 'Exportuje všetky otvorené lehoty ako súbor kalendára s pripomienkami (30 a 7 dní vopred) — možno importovať do Outlooku, kalendára Apple aj Google, v telefóne aj počítači.',
|
||||
|
||||
navQuiz: 'Test gramotnosti v oblasti AI',
|
||||
quizSub: 'Jednoduchý test základov (12 otázok) gramotnosti v oblasti AI podľa čl. 4 — s tlačiteľným certifikátom ako dokladom a lokálnym registrom školení.',
|
||||
quizName: 'Meno účastníka/účastníčky *',
|
||||
quizNamePh: 'Meno a priezvisko',
|
||||
quizSubmit: 'Vyhodnotiť test',
|
||||
quizAnswerAll: 'Zadajte, prosím, meno a odpovedzte na všetky otázky.',
|
||||
quizPassed: 'Úspešne absolvovaný',
|
||||
quizFailed: 'Neabsolvovaný',
|
||||
quizScoreLine: '{correct} z {total} otázok správne ({pct} %) — hranica úspešnosti {pass} %.',
|
||||
quizWhy: 'Vysvetlenie',
|
||||
quizCert: 'Vytlačiť certifikát / PDF',
|
||||
quizRetry: 'Zopakovať test',
|
||||
quizLogTitle: 'Register školení (doklad podľa čl. 4)',
|
||||
quizLogHint: 'Každý pokus sa ukladá lokálne a slúži ako doklad povinnosti gramotnosti v oblasti AI (čl. 4). Register pravidelne tlačte a zakladajte do dokumentácie školení.',
|
||||
quizLogEmpty: 'Zatiaľ žiadne pokusy.',
|
||||
quizLogPrint: 'Vytlačiť register / PDF',
|
||||
quizColScore: 'Výsledok',
|
||||
quizColStatus: 'Stav',
|
||||
|
||||
wizardTitleNew: 'Posúdiť nový systém AI',
|
||||
wizardTitleRe: 'Opätovné posúdenie: {name}',
|
||||
wizardSub: 'Odpovedzte na otázky — riziková trieda sa určí automaticky podľa systematiky aktu o umelej inteligencii.',
|
||||
|
||||
@ -44,6 +44,25 @@ const DE = {
|
||||
calExport: 'In Kalender übernehmen (.ics)',
|
||||
calHint: 'Exportiert alle offenen Fristen als Kalenderdatei mit Erinnerungen (30 und 7 Tage vorher) — importierbar in Outlook, Apple- und Google-Kalender auf Handy und Rechner.',
|
||||
|
||||
navQuiz: 'KI-Kompetenztest',
|
||||
quizSub: 'Einfacher Grundlagentest (12 Fragen) zur KI-Kompetenz nach Art. 4 — mit druckbarem Zertifikat als Nachweis und lokalem Schulungsregister.',
|
||||
quizName: 'Name der Teilnehmerin / des Teilnehmers *',
|
||||
quizNamePh: 'Vor- und Nachname',
|
||||
quizSubmit: 'Test auswerten',
|
||||
quizAnswerAll: 'Bitte Namen eintragen und alle Fragen beantworten.',
|
||||
quizPassed: 'Bestanden',
|
||||
quizFailed: 'Nicht bestanden',
|
||||
quizScoreLine: '{correct} von {total} Fragen richtig ({pct} %) — Bestehensgrenze {pass} %.',
|
||||
quizWhy: 'Erklärung',
|
||||
quizCert: 'Zertifikat drucken / PDF',
|
||||
quizRetry: 'Test wiederholen',
|
||||
quizLogTitle: 'Schulungsregister (Art.-4-Nachweis)',
|
||||
quizLogHint: 'Jeder Testdurchlauf wird lokal gespeichert und dient als Nachweis der KI-Kompetenzpflicht (Art. 4). Register regelmäßig drucken und zur Schulungsdokumentation nehmen.',
|
||||
quizLogEmpty: 'Noch keine Testdurchläufe.',
|
||||
quizLogPrint: 'Register drucken / PDF',
|
||||
quizColScore: 'Ergebnis',
|
||||
quizColStatus: 'Status',
|
||||
|
||||
wizardTitleNew: 'Neues KI-System bewerten',
|
||||
wizardTitleRe: 'Neubewertung: {name}',
|
||||
wizardSub: 'Beantworten Sie die Fragen — die Risikoklasse wird automatisch nach der Systematik des AI Act ermittelt.',
|
||||
@ -219,6 +238,25 @@ const EN: UIStrings = {
|
||||
calExport: 'Add to calendar (.ics)',
|
||||
calHint: 'Exports all open deadlines as a calendar file with reminders (30 and 7 days ahead) — importable into Outlook, Apple and Google Calendar on phone and desktop.',
|
||||
|
||||
navQuiz: 'AI literacy test',
|
||||
quizSub: 'Simple basics test (12 questions) on AI literacy under Art. 4 — with printable certificate as evidence and a local training register.',
|
||||
quizName: 'Participant name *',
|
||||
quizNamePh: 'First and last name',
|
||||
quizSubmit: 'Evaluate test',
|
||||
quizAnswerAll: 'Please enter a name and answer all questions.',
|
||||
quizPassed: 'Passed',
|
||||
quizFailed: 'Not passed',
|
||||
quizScoreLine: '{correct} of {total} questions correct ({pct} %) — pass threshold {pass} %.',
|
||||
quizWhy: 'Explanation',
|
||||
quizCert: 'Print certificate / PDF',
|
||||
quizRetry: 'Retake test',
|
||||
quizLogTitle: 'Training register (Art. 4 evidence)',
|
||||
quizLogHint: 'Each test run is stored locally and serves as evidence of the AI literacy duty (Art. 4). Print the register regularly and file it with your training documentation.',
|
||||
quizLogEmpty: 'No test runs yet.',
|
||||
quizLogPrint: 'Print register / PDF',
|
||||
quizColScore: 'Score',
|
||||
quizColStatus: 'Status',
|
||||
|
||||
wizardTitleNew: 'Assess a new AI system',
|
||||
wizardTitleRe: 'Reassessment: {name}',
|
||||
wizardSub: 'Answer the questions — the risk class is determined automatically following the AI Act’s system.',
|
||||
|
||||
@ -1,7 +1,26 @@
|
||||
import type { TrainingRecord } from '../engine/quiz';
|
||||
import type { AISystem, CompanyProfile } from '../engine/types';
|
||||
|
||||
const STORAGE_KEY = 'ai-act-kompass/systems/v1';
|
||||
const PROFILE_KEY = 'ai-act-kompass/profile/v1';
|
||||
const TRAINING_KEY = 'ai-act-kompass/training/v1';
|
||||
|
||||
/** Lädt das Schulungsregister aus dem lokalen Speicher (fail-safe). */
|
||||
export function loadTraining(): readonly TrainingRecord[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(TRAINING_KEY);
|
||||
if (!raw) return [];
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? (parsed as TrainingRecord[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Persistiert das Schulungsregister im lokalen Speicher. */
|
||||
export function saveTraining(records: readonly TrainingRecord[]): void {
|
||||
localStorage.setItem(TRAINING_KEY, JSON.stringify(records));
|
||||
}
|
||||
|
||||
/** Format der Export-/Import-Datei. */
|
||||
export interface ExportFile {
|
||||
@ -127,4 +146,5 @@ export function importData(json: string): ImportResult {
|
||||
export function clearAll(): void {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(PROFILE_KEY);
|
||||
localStorage.removeItem(TRAINING_KEY);
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { TrainingRecord } from '../engine/quiz';
|
||||
import type { AISystem, CompanyProfile, ObligationStatus } from '../engine/types';
|
||||
import { useI18n } from '../i18n/context';
|
||||
import { LOCALES } from '../i18n/types';
|
||||
import { fmt } from '../i18n/ui';
|
||||
import { loadProfile, loadSystems, saveProfile, saveSystems } from '../store/storage';
|
||||
import { loadProfile, loadSystems, loadTraining, saveProfile, saveSystems, saveTraining } from '../store/storage';
|
||||
import { ChecklistView } from './ChecklistView';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { DisclaimerGate } from './DisclaimerGate';
|
||||
import { QuizView } from './QuizView';
|
||||
import { SettingsView } from './SettingsView';
|
||||
import { SystemDetail } from './SystemDetail';
|
||||
import { TemplatesView } from './TemplatesView';
|
||||
@ -16,6 +18,7 @@ type View =
|
||||
| { readonly kind: 'dashboard' }
|
||||
| { readonly kind: 'tasks' }
|
||||
| { readonly kind: 'templates' }
|
||||
| { readonly kind: 'quiz' }
|
||||
| { readonly kind: 'wizard'; readonly systemId: string | null }
|
||||
| { readonly kind: 'detail'; readonly systemId: string }
|
||||
| { readonly kind: 'settings' };
|
||||
@ -25,6 +28,7 @@ export function App(): JSX.Element {
|
||||
const { t, locale, setLocale } = useI18n();
|
||||
const [systems, setSystems] = useState<readonly AISystem[]>(() => loadSystems());
|
||||
const [profile, setProfile] = useState<CompanyProfile>(() => loadProfile());
|
||||
const [training, setTraining] = useState<readonly TrainingRecord[]>(() => loadTraining());
|
||||
const [view, setView] = useState<View>({ kind: 'dashboard' });
|
||||
|
||||
useEffect(() => {
|
||||
@ -35,6 +39,10 @@ export function App(): JSX.Element {
|
||||
saveProfile(profile);
|
||||
}, [profile]);
|
||||
|
||||
useEffect(() => {
|
||||
saveTraining(training);
|
||||
}, [training]);
|
||||
|
||||
const findSystem = (id: string): AISystem | undefined => systems.find((s) => s.id === id);
|
||||
|
||||
const upsert = (system: AISystem): void => {
|
||||
@ -96,6 +104,12 @@ export function App(): JSX.Element {
|
||||
>
|
||||
{t.navTemplates}
|
||||
</button>
|
||||
<button
|
||||
className={view.kind === 'quiz' ? 'active' : ''}
|
||||
onClick={() => setView({ kind: 'quiz' })}
|
||||
>
|
||||
{t.navQuiz}
|
||||
</button>
|
||||
<button
|
||||
className={view.kind === 'settings' ? 'active' : ''}
|
||||
onClick={() => setView({ kind: 'settings' })}
|
||||
@ -148,6 +162,14 @@ export function App(): JSX.Element {
|
||||
|
||||
{view.kind === 'templates' && <TemplatesView systems={systems} profile={profile} />}
|
||||
|
||||
{view.kind === 'quiz' && (
|
||||
<QuizView
|
||||
profile={profile}
|
||||
records={training}
|
||||
onRecord={(record) => setTraining((prev) => [record, ...prev])}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view.kind === 'wizard' && (
|
||||
<Wizard
|
||||
existing={view.systemId ? (findSystem(view.systemId) ?? null) : null}
|
||||
@ -190,7 +212,10 @@ export function App(): JSX.Element {
|
||||
setSystems(imported);
|
||||
if (importedProfile) setProfile(importedProfile);
|
||||
}}
|
||||
onClear={() => setSystems([])}
|
||||
onClear={() => {
|
||||
setSystems([]);
|
||||
setTraining([]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
197
ai-act-kompass/src/ui/QuizView.tsx
Normal file
197
ai-act-kompass/src/ui/QuizView.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { certificateDoc, getQuiz, 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';
|
||||
import { useI18n } from '../i18n/context';
|
||||
import { fmt } from '../i18n/ui';
|
||||
import { printDocument } from '../print/print';
|
||||
import { todayISO, uiDate } from './common';
|
||||
|
||||
interface QuizViewProps {
|
||||
readonly profile: CompanyProfile;
|
||||
readonly records: readonly TrainingRecord[];
|
||||
readonly onRecord: (record: TrainingRecord) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* KI-Kompetenztest (Art. 4): einfacher Grundlagentest mit Auswertung,
|
||||
* druckbarem Zertifikat und lokalem Schulungsregister als Nachweis.
|
||||
*/
|
||||
export function QuizView({ profile, records, onRecord }: QuizViewProps): JSX.Element {
|
||||
const { t, locale, pack } = useI18n();
|
||||
const today = todayISO();
|
||||
const questions = useMemo(() => getQuiz(locale), [locale]);
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [answers, setAnswers] = useState<readonly (number | null)[]>(() =>
|
||||
questions.map(() => null),
|
||||
);
|
||||
const [result, setResult] = useState<QuizScore | null>(null);
|
||||
|
||||
const complete = name.trim() !== '' && answers.every((a) => a !== null);
|
||||
|
||||
const submit = (): void => {
|
||||
const score = scoreQuiz(answers);
|
||||
setResult(score);
|
||||
onRecord({
|
||||
id: crypto.randomUUID(),
|
||||
name: name.trim(),
|
||||
date: today,
|
||||
scorePct: score.pct,
|
||||
passed: score.passed,
|
||||
});
|
||||
};
|
||||
|
||||
const reset = (): void => {
|
||||
setAnswers(questions.map(() => null));
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{t.navQuiz}</h1>
|
||||
<p className="sub">{t.quizSub}</p>
|
||||
|
||||
{result === null ? (
|
||||
<>
|
||||
<div className="panel">
|
||||
<label className="field">
|
||||
<span>{t.quizName}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
placeholder={t.quizNamePh}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{questions.map((q, qi) => (
|
||||
<div key={q.id} className="panel">
|
||||
<h2 style={{ marginTop: 0 }}>
|
||||
{qi + 1}. {q.question}
|
||||
</h2>
|
||||
{q.options.map((option, oi) => (
|
||||
<label key={oi} className="check">
|
||||
<input
|
||||
type="radio"
|
||||
name={q.id}
|
||||
checked={answers[qi] === oi}
|
||||
onChange={() =>
|
||||
setAnswers(answers.map((a, i) => (i === qi ? oi : a)))
|
||||
}
|
||||
/>
|
||||
<span>{option}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="wizard-nav">
|
||||
<span className="hint">{!complete ? t.quizAnswerAll : ''}</span>
|
||||
<button className="primary" disabled={!complete} onClick={submit}>
|
||||
{t.quizSubmit}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="panel">
|
||||
<h2 style={{ marginTop: 0 }}>
|
||||
<span className={`badge ${result.passed ? 'minimal' : 'high'}`}>
|
||||
{result.passed ? t.quizPassed : t.quizFailed}
|
||||
</span>
|
||||
</h2>
|
||||
<p>
|
||||
{fmt(t.quizScoreLine, {
|
||||
correct: result.correct,
|
||||
total: result.total,
|
||||
pct: result.pct,
|
||||
pass: QUIZ_PASS_PCT,
|
||||
})}
|
||||
</p>
|
||||
<div className="toolbar">
|
||||
<button
|
||||
className="primary"
|
||||
onClick={() =>
|
||||
printDocument(
|
||||
`ki-kompetenz-${name.trim()}-${today}`,
|
||||
certificateDoc(name.trim(), result, pack, today, profile, locale),
|
||||
profile,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t.quizCert}
|
||||
</button>
|
||||
<button className="ghost" onClick={reset}>
|
||||
{t.quizRetry}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2>{t.quizWhy}</h2>
|
||||
<ul className="reasons">
|
||||
{questions.map((q, qi) => (
|
||||
<li key={q.id}>
|
||||
<strong>
|
||||
{answers[qi] === q.correctIndex ? '✓' : '✗'} {qi + 1}.
|
||||
</strong>{' '}
|
||||
{q.explanation}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="panel">
|
||||
<div className="panel-head">
|
||||
<h2 style={{ margin: 0 }}>{t.quizLogTitle}</h2>
|
||||
{records.length > 0 && (
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={() =>
|
||||
printDocument(
|
||||
`schulungsregister-${today}`,
|
||||
trainingLogDoc(records, pack, today, profile, locale),
|
||||
profile,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t.quizLogPrint}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="hint">{t.quizLogHint}</p>
|
||||
{records.length === 0 ? (
|
||||
<p className="hint">{t.quizLogEmpty}</p>
|
||||
) : (
|
||||
<div className="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t.colUpdated}</th>
|
||||
<th>{t.colOwner}</th>
|
||||
<th>{t.quizColScore}</th>
|
||||
<th>{t.quizColStatus}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td>{uiDate(r.date, locale)}</td>
|
||||
<td>{r.name}</td>
|
||||
<td>{r.scorePct} %</td>
|
||||
<td>
|
||||
<span className={`badge ${r.passed ? 'minimal' : 'high'}`}>
|
||||
{r.passed ? t.quizPassed : t.quizFailed}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user