diff --git a/ai-act-kompass/src/engine/quiz.test.ts b/ai-act-kompass/src/engine/quiz.test.ts new file mode 100644 index 0000000..0051319 --- /dev/null +++ b/ai-act-kompass/src/engine/quiz.test.ts @@ -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 %'); + }); +}); diff --git a/ai-act-kompass/src/engine/quiz.ts b/ai-act-kompass/src/engine/quiz.ts new file mode 100644 index 0000000..8cc4de9 --- /dev/null +++ b/ai-act-kompass/src/engine/quiz.ts @@ -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> = { + 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'); +} diff --git a/ai-act-kompass/src/engine/quizContent.ts b/ai-act-kompass/src/engine/quizContent.ts new file mode 100644 index 0000000..200f867 --- /dev/null +++ b/ai-act-kompass/src/engine/quizContent.ts @@ -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.', + ), + }, +]; diff --git a/ai-act-kompass/src/i18n/ui.cs.ts b/ai-act-kompass/src/i18n/ui.cs.ts index ebb1c69..adc9049 100644 --- a/ai-act-kompass/src/i18n/ui.cs.ts +++ b/ai-act-kompass/src/i18n/ui.cs.ts @@ -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.', diff --git a/ai-act-kompass/src/i18n/ui.es.ts b/ai-act-kompass/src/i18n/ui.es.ts index 32c5699..c14a428 100644 --- a/ai-act-kompass/src/i18n/ui.es.ts +++ b/ai-act-kompass/src/i18n/ui.es.ts @@ -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.', diff --git a/ai-act-kompass/src/i18n/ui.fr.ts b/ai-act-kompass/src/i18n/ui.fr.ts index 8f97e51..71a8656 100644 --- a/ai-act-kompass/src/i18n/ui.fr.ts +++ b/ai-act-kompass/src/i18n/ui.fr.ts @@ -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.', diff --git a/ai-act-kompass/src/i18n/ui.it.ts b/ai-act-kompass/src/i18n/ui.it.ts index b6c4439..11212be 100644 --- a/ai-act-kompass/src/i18n/ui.it.ts +++ b/ai-act-kompass/src/i18n/ui.it.ts @@ -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.', diff --git a/ai-act-kompass/src/i18n/ui.pl.ts b/ai-act-kompass/src/i18n/ui.pl.ts index 22d21fb..dd4b605 100644 --- a/ai-act-kompass/src/i18n/ui.pl.ts +++ b/ai-act-kompass/src/i18n/ui.pl.ts @@ -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.', diff --git a/ai-act-kompass/src/i18n/ui.ro.ts b/ai-act-kompass/src/i18n/ui.ro.ts index 6f8657c..fdd0f20 100644 --- a/ai-act-kompass/src/i18n/ui.ro.ts +++ b/ai-act-kompass/src/i18n/ui.ro.ts @@ -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.', diff --git a/ai-act-kompass/src/i18n/ui.sk.ts b/ai-act-kompass/src/i18n/ui.sk.ts index d0bdd1d..804ff79 100644 --- a/ai-act-kompass/src/i18n/ui.sk.ts +++ b/ai-act-kompass/src/i18n/ui.sk.ts @@ -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.', diff --git a/ai-act-kompass/src/i18n/ui.ts b/ai-act-kompass/src/i18n/ui.ts index d0250b9..1267d57 100644 --- a/ai-act-kompass/src/i18n/ui.ts +++ b/ai-act-kompass/src/i18n/ui.ts @@ -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.', diff --git a/ai-act-kompass/src/store/storage.ts b/ai-act-kompass/src/store/storage.ts index 463cc71..2df970a 100644 --- a/ai-act-kompass/src/store/storage.ts +++ b/ai-act-kompass/src/store/storage.ts @@ -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); } diff --git a/ai-act-kompass/src/ui/App.tsx b/ai-act-kompass/src/ui/App.tsx index 35892f6..8700f80 100644 --- a/ai-act-kompass/src/ui/App.tsx +++ b/ai-act-kompass/src/ui/App.tsx @@ -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(() => loadSystems()); const [profile, setProfile] = useState(() => loadProfile()); + const [training, setTraining] = useState(() => loadTraining()); const [view, setView] = useState({ 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} + + + + ) : ( +
+

+ + {result.passed ? t.quizPassed : t.quizFailed} + +

+

+ {fmt(t.quizScoreLine, { + correct: result.correct, + total: result.total, + pct: result.pct, + pass: QUIZ_PASS_PCT, + })} +

+
+ + +
+ +

{t.quizWhy}

+
    + {questions.map((q, qi) => ( +
  • + + {answers[qi] === q.correctIndex ? '✓' : '✗'} {qi + 1}. + {' '} + {q.explanation} +
  • + ))} +
+
+ )} + +
+
+

{t.quizLogTitle}

+ {records.length > 0 && ( + + )} +
+

{t.quizLogHint}

+ {records.length === 0 ? ( +

{t.quizLogEmpty}

+ ) : ( +
+ + + + + + + + + + + {records.map((r) => ( + + + + + + + ))} + +
{t.colUpdated}{t.colOwner}{t.quizColScore}{t.quizColStatus}
{uiDate(r.date, locale)}{r.name}{r.scorePct} % + + {r.passed ? t.quizPassed : t.quizFailed} + +
+
+ )} +
+ + ); +}