feat(ai-act-kompass): i18n, national modules, task checklist, legal gate, new documents

- Full i18n (DE/EN): UI, rules engine, content pack and generated
  documents; language switcher, browser-language default, architecture
  extensible to further EU languages via localized source texts
- National country modules (opt-in per company profile): DE (KI-MIG/
  BNetzA supervision, works council co-determination Sec. 87 BetrVG,
  GDPR/BDSG), AT (RTR AI service desk), IT (Law 132/2025: employer info
  duty, deepfake criminal liability, sector decrees), ES (AESIA,
  sandbox RD 817/2023), FR (CNIL guidance), NL (algorithm register);
  merged into obligation derivation, checklists and documents
- New EU obligations: corrective actions (Art. 20/21), authorised
  representative (Art. 22), EU database registration for public-body
  deployers (Art. 49(3)), post-market monitoring (Art. 72), serious
  incident reporting (Art. 73), GDPR DPIA (Art. 35 GDPR / 26(9))
- New documents: AI procurement requirements specification (MUST/SHOULD
  matrix for vendors) and EU declaration of conformity draft (Annex V)
- Cross-system checkable task list (due now / upcoming / done) with
  progress bar, one-tap toggling, per-system links
- Legal safety: first-run acknowledgement gate (working aid, no legal
  services), legal notice panel in settings, result-screen disclaimer,
  disclaimers on every document
- Responsive layout for phone/tablet (top-bar nav, scrollable tables,
  larger tap targets)
- 12 new tests (44 total): EN pack parity, national merge, checklist
  grouping, new document generators

Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
This commit is contained in:
Claude 2026-07-04 09:16:14 +00:00
parent 06574c5cfd
commit cd31274738
No known key found for this signature in database
29 changed files with 2948 additions and 818 deletions

View File

@ -1,15 +1,18 @@
# 🧭 AI-Act-Kompass
# AI-Act-Kompass
**Standalone EU-AI-Act-Compliance-Toolkit für KMU.** Local-first: keine Cloud, keine Telemetrie, keine Datenübertragung — alle Daten bleiben im Browser des Nutzers (localStorage, mit JSON-Export/-Import).
**Standalone EU-AI-Act-Compliance-Toolkit für europäische Unternehmen (KMU-Fokus).** Local-first: keine Cloud, keine Telemetrie, keine Datenübertragung — alle Daten bleiben im Browser des Nutzers (localStorage, mit JSON-Export/-Import). Mehrsprachig (DE/EN, erweiterbar), responsiv für Handy, Tablet und Desktop.
> ⚠️ Strukturierte Arbeitshilfe auf Basis der Verordnung (EU) 2024/1689 — **keine Rechtsberatung**.
> ⚠️ Strukturierte Arbeitshilfe auf Basis der Verordnung (EU) 2024/1689 — **keine Rechtsberatung** und keine Rechtsdienstleistung i. S. d. RDG. Beim ersten Start bestätigungspflichtiger rechtlicher Hinweis; Disclaimer in App und allen Dokumenten.
## Was es kann
- **Bewertungs-Wizard**: erfasst ein KI-System und klassifiziert es automatisch nach der Systematik des AI Act — KI-Definition (Art. 3 Nr. 1) → verbotene Praktiken (Art. 5) → Hochrisiko (Art. 6, Annex I/III, inkl. Ausnahme Art. 6 Abs. 3 und Profiling-Sperre) → Transparenzpflichten (Art. 50) → minimales Risiko. GPAI-Pflichten (Art. 53 ff.) werden separat geflaggt.
- **Aufgaben & Fristen**: systemübergreifende, abhakbare Pflichtenliste („Jetzt fällig / Kommt auf Sie zu / Erledigt") mit Fortschrittsanzeige — mobiltauglich.
- **KI-Inventar**: alle Systeme des Unternehmens mit Risikoklasse, Rolle und Verantwortlichen — exportierbar als Markdown (AI Asset Register).
- **Pflichten-Checklisten**: rollenspezifisch (Anbieter/Betreiber/Einführer/Händler) abgeleitete Pflichten mit Rechtsgrundlage, Geltungsfrist und Umsetzungsstatus.
- **Dokumenten-Generator**: Risikoklassifizierungs-Bericht, Maßnahmenplan und Annex-IV-Grundgerüst (technische Dokumentation) als Markdown.
- **Pflichten-Checklisten**: rollenspezifisch (Anbieter/Betreiber/Einführer/Händler) abgeleitete EU-Pflichten (Art. 473) mit Rechtsgrundlage, Geltungsfrist und Umsetzungsstatus.
- **Länder-Module** (zuschaltbar): nationale Zusatzanforderungen für DE (KI-MIG/BNetzA, § 87 BetrVG, DSGVO/BDSG), AT (RTR-KI-Servicestelle), IT (Legge 132/2025: Arbeitgeber-Info, Deepfake-Strafnormen, Sektorregeln), ES (AESIA, Sandbox RD 817/2023), FR (CNIL), NL (Algorithmenregister). Bei grenzüberschreitender Zusammenarbeit mehrere Länder aktivieren.
- **Dokumenten-Generator** (Markdown + Druck/PDF mit Firmen-Briefkopf): Risikobericht, Maßnahmenplan, Annex-IV-Grundgerüst, **Lastenheft KI-Beschaffung** (AI-Act-Anforderungen an Anbieter, MUSS/SOLL) und **EU-Konformitätserklärung** (Entwurf nach Art. 47/Annex V).
- **Firmenprofil**: Logo, Name, Kontaktdaten — erscheinen in Briefkopf und Fußzeile aller gedruckten Dokumente.
- **Fristen-Übersicht**: alle Geltungsfristen des AI Act inkl. der Digital-Omnibus-Verschiebung (Annex III → 02.12.2027).
## Architektur

View File

@ -1,389 +1,100 @@
import type { ContentPack } from '../engine/types';
import type {
ContentItem,
ContentPack,
Deadline,
NationalPack,
Obligation,
} from '../engine/types';
import type { Locale, LText } from '../i18n/types';
import {
AI_DEFINITION_CRITERIA,
ANNEX_III_CATEGORIES,
DEADLINES,
PROHIBITED_PRACTICES,
TRANSPARENCY_TRIGGERS,
} from './packCore';
import { NATIONAL_PACKS } from './packNational';
import { OBLIGATIONS } from './packObligations';
import type { ContentItemSrc, DeadlineSrc, NationalPackSrc, ObligationSrc } from './sourceTypes';
/**
* Regulatorischer Content-Pack v2026.07.
*
* Kodifiziert den Stand der Verordnung (EU) 2024/1689 (AI Act) inkl. der
* durch das Digital-Omnibus-Paket verschobenen Annex-III-Fristen.
* Dieser Pack ist bewusst als Daten (nicht Code) modelliert, damit er
* später über einen signierten Update-Feed ausgetauscht werden kann.
* durch das Digital-Omnibus-Paket verschobenen Annex-III-Fristen sowie
* nationale Zusatzmodule (DE, AT, IT, ES, FR, NL). Quelltexte sind
* lokalisiert (DE/EN) und werden hier je Sprache aufgelöst vorbereitet
* für einen späteren signierten Update-Feed.
*
* WICHTIG: Inhalte sind eine strukturierte Arbeitshilfe, keine Rechtsberatung.
*/
export const CONTENT_PACK: ContentPack = {
version: '2026.07.0',
releasedAt: '2026-07-04',
legalBasis: 'Verordnung (EU) 2024/1689 (AI Act), Stand Juli 2026',
const VERSION = '2026.07.1';
const RELEASED_AT = '2026-07-04';
aiDefinitionCriteria: [
{
id: 'def-autonomy',
label: 'Das System arbeitet mit einem gewissen Grad an Autonomie (nicht rein regelbasiert deterministisch durch den Nutzer gesteuert).',
legalRef: 'Art. 3 Nr. 1',
},
{
id: 'def-inference',
label: 'Das System leitet aus Eingaben ab, wie Ausgaben (Vorhersagen, Inhalte, Empfehlungen, Entscheidungen) erzeugt werden.',
legalRef: 'Art. 3 Nr. 1',
},
{
id: 'def-influence',
label: 'Die Ausgaben können physische oder virtuelle Umgebungen beeinflussen.',
legalRef: 'Art. 3 Nr. 1',
},
],
prohibitedPractices: [
{
id: 'proh-manipulation',
label: 'Unterschwellige oder gezielt manipulative Techniken, die Verhalten wesentlich verändern und erheblichen Schaden verursachen (können).',
legalRef: 'Art. 5 Abs. 1 lit. a',
},
{
id: 'proh-vulnerability',
label: 'Ausnutzung von Schwächen aufgrund von Alter, Behinderung oder sozialer/wirtschaftlicher Lage mit erheblichem Schadensrisiko.',
legalRef: 'Art. 5 Abs. 1 lit. b',
},
{
id: 'proh-social-scoring',
label: 'Social Scoring: Bewertung von Personen anhand ihres Sozialverhaltens mit ungerechtfertigter Schlechterstellung.',
legalRef: 'Art. 5 Abs. 1 lit. c',
},
{
id: 'proh-predictive-policing',
label: 'Risikobewertung von Straftaten allein auf Basis von Profiling oder Persönlichkeitsmerkmalen.',
legalRef: 'Art. 5 Abs. 1 lit. d',
},
{
id: 'proh-face-scraping',
label: 'Ungezieltes Auslesen (Scraping) von Gesichtsbildern aus Internet oder Überwachungskameras zum Aufbau von Gesichtsdatenbanken.',
legalRef: 'Art. 5 Abs. 1 lit. e',
},
{
id: 'proh-emotion-workplace',
label: 'Emotionserkennung am Arbeitsplatz oder in Bildungseinrichtungen (außer aus medizinischen oder Sicherheitsgründen).',
legalRef: 'Art. 5 Abs. 1 lit. f',
},
{
id: 'proh-biometric-categorisation',
label: 'Biometrische Kategorisierung zur Ableitung sensibler Merkmale (z. B. Ethnie, politische Meinung, sexuelle Orientierung).',
legalRef: 'Art. 5 Abs. 1 lit. g',
},
{
id: 'proh-rbi',
label: 'Biometrische Echtzeit-Fernidentifizierung in öffentlich zugänglichen Räumen zu Strafverfolgungszwecken (enge Ausnahmen).',
legalRef: 'Art. 5 Abs. 1 lit. h',
},
],
annexIIICategories: [
{
id: 'a3-biometrics',
label: 'Biometrie: Fernidentifizierung, biometrische Kategorisierung, Emotionserkennung (soweit nicht verboten).',
legalRef: 'Annex III Nr. 1',
},
{
id: 'a3-critical-infra',
label: 'Sicherheitskomponenten kritischer Infrastruktur (Verkehr, Wasser, Gas, Wärme, Strom, digitale Infrastruktur).',
legalRef: 'Annex III Nr. 2',
},
{
id: 'a3-education',
label: 'Bildung: Zulassung, Bewertung von Lernergebnissen, Prüfungsüberwachung.',
legalRef: 'Annex III Nr. 3',
},
{
id: 'a3-employment',
label: 'Beschäftigung: Recruiting/Bewerberauswahl, Beförderung/Kündigung, Aufgabenzuweisung, Leistungsüberwachung.',
legalRef: 'Annex III Nr. 4',
hint: 'Häufigster KMU-Fall: KI-gestütztes Bewerber-Screening.',
},
{
id: 'a3-essential-services',
label: 'Wesentliche Dienste: Kreditwürdigkeitsprüfung, Risiko-Pricing bei Lebens-/Krankenversicherung, Notruf-Triage, Sozialleistungen.',
legalRef: 'Annex III Nr. 5',
},
{
id: 'a3-law-enforcement',
label: 'Strafverfolgung (Beweismittelbewertung, Rückfallrisiko u. a.).',
legalRef: 'Annex III Nr. 6',
},
{
id: 'a3-migration',
label: 'Migration, Asyl, Grenzkontrolle.',
legalRef: 'Annex III Nr. 7',
},
{
id: 'a3-justice',
label: 'Rechtspflege und demokratische Prozesse (Unterstützung richterlicher Entscheidungen, Wahlbeeinflussung).',
legalRef: 'Annex III Nr. 8',
},
],
transparencyTriggers: [
{
id: 'tr-chatbot',
label: 'Das System interagiert direkt mit Menschen (z. B. Chatbot, Voicebot).',
legalRef: 'Art. 50 Abs. 1',
hint: 'Nutzer müssen erkennen können, dass sie mit KI interagieren.',
},
{
id: 'tr-synthetic-content',
label: 'Das System erzeugt synthetische Audio-, Bild-, Video- oder Textinhalte.',
legalRef: 'Art. 50 Abs. 2',
hint: 'Maschinenlesbare Kennzeichnung der Ausgaben erforderlich.',
},
{
id: 'tr-emotion',
label: 'Emotionserkennung oder biometrische Kategorisierung wird eingesetzt (soweit erlaubt).',
legalRef: 'Art. 50 Abs. 3',
},
{
id: 'tr-deepfake',
label: 'Das System erzeugt oder manipuliert Deepfakes (Bild/Audio/Video realer Personen, Orte oder Ereignisse).',
legalRef: 'Art. 50 Abs. 4',
},
],
obligations: [
// ── Alle Risikoklassen / alle Rollen ──────────────────────────────
{
id: 'ob-ai-literacy',
title: 'KI-Kompetenz des Personals sicherstellen',
articles: 'Art. 4',
description:
'Mitarbeitende, die KI-Systeme betreiben oder nutzen, müssen über ausreichende KI-Kompetenz verfügen (Schulungen, Richtlinien). Gilt seit 2. Februar 2025 für alle Anbieter und Betreiber.',
deadline: '2025-02-02',
roles: ['provider', 'deployer', 'importer', 'distributor'],
riskClasses: ['high', 'limited', 'minimal'],
},
// ── Hochrisiko: Anbieter ──────────────────────────────────────────
{
id: 'ob-risk-mgmt',
title: 'Risikomanagementsystem einrichten',
articles: 'Art. 9',
description:
'Kontinuierlicher, iterativer Risikomanagementprozess über den gesamten Lebenszyklus: Risiken identifizieren, bewerten, mindern, testen.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-data-governance',
title: 'Daten-Governance für Trainings-/Test-/Validierungsdaten',
articles: 'Art. 10',
description:
'Datensätze müssen relevant, repräsentativ, möglichst fehlerfrei und vollständig sein; Bias-Prüfung und geeignete Erhebungsverfahren dokumentieren.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-tech-doc',
title: 'Technische Dokumentation (Annex IV) erstellen',
articles: 'Art. 11, Annex IV',
description:
'Vollständige technische Dokumentation vor Inverkehrbringen; für KMU gelten vereinfachte Formulare. Der Dokumenten-Generator dieses Tools erzeugt das Grundgerüst.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-logging',
title: 'Protokollierung (Logging) technisch ermöglichen',
articles: 'Art. 12',
description:
'Automatische Aufzeichnung von Ereignissen über den Lebenszyklus, um Rückverfolgbarkeit und Marktüberwachung zu ermöglichen.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-transparency-instructions',
title: 'Betriebsanleitung und Transparenzinformationen bereitstellen',
articles: 'Art. 13',
description:
'Klare Gebrauchsanweisung: Zweckbestimmung, Genauigkeit, Grenzen, menschliche Aufsicht, erwartete Lebensdauer und Wartung.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-human-oversight',
title: 'Menschliche Aufsicht konzipieren',
articles: 'Art. 14',
description:
'Das System muss wirksam durch Menschen beaufsichtigt werden können (Eingriffs-/Abbruchmöglichkeiten, Automation-Bias-Vorkehrungen).',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-accuracy-robustness',
title: 'Genauigkeit, Robustheit und Cybersicherheit gewährleisten',
articles: 'Art. 15',
description:
'Angemessenes Niveau an Genauigkeit und Widerstandsfähigkeit, inkl. Schutz gegen KI-spezifische Angriffe (Data Poisoning, Adversarial Examples, Prompt Injection).',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-qms',
title: 'Qualitätsmanagementsystem einführen',
articles: 'Art. 17',
description:
'Dokumentiertes QMS: Compliance-Strategie, Prüfverfahren, Datenmanagement, Risikomanagement, Meldeverfahren, Verantwortlichkeiten.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-conformity',
title: 'Konformitätsbewertung durchführen, CE-Kennzeichnung, EU-Datenbank-Registrierung',
articles: 'Art. 43, 48, 49',
description:
'Konformitätsbewertungsverfahren abschließen, EU-Konformitätserklärung ausstellen, CE-Kennzeichnung anbringen und System in der EU-Datenbank registrieren.',
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
// ── Hochrisiko: Betreiber ─────────────────────────────────────────
{
id: 'ob-deployer-use',
title: 'Nutzung gemäß Betriebsanleitung sicherstellen',
articles: 'Art. 26 Abs. 1',
description:
'Technische und organisatorische Maßnahmen, damit das System entsprechend der Gebrauchsanweisung des Anbieters genutzt wird.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-oversight',
title: 'Kompetente menschliche Aufsicht zuweisen',
articles: 'Art. 26 Abs. 2',
description:
'Aufsicht durch Personen mit erforderlicher Kompetenz, Ausbildung und Befugnis.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-input',
title: 'Relevanz und Repräsentativität der Eingabedaten sicherstellen',
articles: 'Art. 26 Abs. 4',
description:
'Soweit der Betreiber die Eingabedaten kontrolliert, müssen diese für die Zweckbestimmung relevant und hinreichend repräsentativ sein.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-monitoring',
title: 'Betrieb überwachen, Vorfälle melden, Logs aufbewahren',
articles: 'Art. 26 Abs. 56',
description:
'Betrieb anhand der Anleitung überwachen; bei Risiken Anbieter/Behörden informieren; automatisch erzeugte Logs mind. 6 Monate aufbewahren.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-workers',
title: 'Beschäftigte vor Einsatz am Arbeitsplatz informieren',
articles: 'Art. 26 Abs. 7',
description:
'Vor Inbetriebnahme eines Hochrisiko-Systems am Arbeitsplatz sind Arbeitnehmervertreter und betroffene Beschäftigte zu informieren.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-fria',
title: 'Grundrechte-Folgenabschätzung (FRIA) prüfen',
articles: 'Art. 27',
description:
'Pflicht für öffentliche Stellen und private Anbieter öffentlicher Dienste sowie bestimmte Annex-III-Fälle (z. B. Kreditwürdigkeit, Versicherung): Folgen für Grundrechte vor Einsatz bewerten.',
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
// ── Importeur / Händler ───────────────────────────────────────────
{
id: 'ob-importer-verify',
title: 'Konformität vor Inverkehrbringen prüfen',
articles: 'Art. 23',
description:
'Importeure müssen prüfen: Konformitätsbewertung durchgeführt, technische Dokumentation vorhanden, CE-Kennzeichnung und Ansprechpartner benannt.',
deadline: '2027-12-02',
roles: ['importer'],
riskClasses: ['high'],
},
{
id: 'ob-distributor-verify',
title: 'Sorgfaltspflichten der Vertriebskette erfüllen',
articles: 'Art. 24',
description:
'Händler prüfen CE-Kennzeichnung, Konformitätserklärung und Gebrauchsanweisung; bei Zweifeln keine Bereitstellung.',
deadline: '2027-12-02',
roles: ['distributor'],
riskClasses: ['high'],
},
// ── Transparenzpflichten (limited) ────────────────────────────────
{
id: 'ob-transparency-art50',
title: 'Transparenzpflichten nach Art. 50 umsetzen',
articles: 'Art. 50',
description:
'Je nach Auslöser: KI-Interaktion offenlegen (Chatbots), synthetische Inhalte maschinenlesbar kennzeichnen, Deepfakes sichtbar kennzeichnen, über Emotionserkennung informieren.',
deadline: '2026-08-02',
roles: ['provider', 'deployer'],
riskClasses: ['high', 'limited'],
},
// ── GPAI ──────────────────────────────────────────────────────────
{
id: 'ob-gpai',
title: 'GPAI-Modellpflichten erfüllen',
articles: 'Art. 5355',
description:
'Für Anbieter von GPAI-Modellen: technische Modell-Dokumentation, Informationen für nachgelagerte Anbieter, Urheberrechts-Policy, Zusammenfassung der Trainingsdaten. Bei systemischem Risiko zusätzlich Evaluierungen und Incident-Meldungen.',
deadline: '2025-08-02',
roles: ['provider'],
riskClasses: ['high', 'limited', 'minimal'],
},
],
deadlines: [
{
id: 'dl-prohibitions',
date: '2025-02-02',
label: 'Verbote & KI-Kompetenz',
description: 'Verbotene Praktiken (Art. 5) und KI-Kompetenzpflicht (Art. 4) gelten. Bereits in Kraft.',
},
{
id: 'dl-gpai',
date: '2025-08-02',
label: 'GPAI-Pflichten',
description: 'Pflichten für Anbieter von GPAI-Modellen (Art. 53 ff.) gelten. Bereits in Kraft.',
},
{
id: 'dl-general',
date: '2026-08-02',
label: 'Allgemeine Geltung',
description: 'Transparenzpflichten (Art. 50), Sanktionsregime und Governance-Struktur gelten.',
},
{
id: 'dl-annex3',
date: '2027-12-02',
label: 'Hochrisiko (Annex III)',
description: 'Pflichten für Annex-III-Hochrisiko-Systeme gelten (durch Digital Omnibus von Aug 2026 verschoben).',
},
{
id: 'dl-annex1',
date: '2028-08-02',
label: 'Hochrisiko (Annex I)',
description: 'Pflichten für KI als Sicherheitsbauteil regulierter Produkte (Annex I) gelten.',
},
],
const LEGAL_BASIS: LText = {
de: 'Verordnung (EU) 2024/1689 (AI Act) + nationale Module, Stand Juli 2026',
en: 'Regulation (EU) 2024/1689 (AI Act) + national modules, as of July 2026',
};
function resolveItem(src: ContentItemSrc, locale: Locale): ContentItem {
return {
id: src.id,
label: src.label[locale],
legalRef: src.legalRef,
...(src.hint ? { hint: src.hint[locale] } : {}),
};
}
function resolveObligation(src: ObligationSrc, locale: Locale): Obligation {
return {
id: src.id,
title: src.title[locale],
articles: src.articles,
description: src.description[locale],
deadline: src.deadline,
roles: src.roles,
riskClasses: src.riskClasses,
};
}
function resolveDeadline(src: DeadlineSrc, locale: Locale): Deadline {
return {
id: src.id,
date: src.date,
label: src.label[locale],
description: src.description[locale],
};
}
function resolveNational(src: NationalPackSrc, locale: Locale): NationalPack {
return {
code: src.code,
country: src.country[locale],
authority: src.authority[locale],
items: src.items.map((i) => resolveObligation(i, locale)),
};
}
/**
* Liefert den Content-Pack in der gewünschten Sprache.
*
* @param locale - Zielsprache (de/en)
* @returns Vollständig aufgelöster Content-Pack
*/
export function getContentPack(locale: Locale): ContentPack {
return {
version: VERSION,
releasedAt: RELEASED_AT,
legalBasis: LEGAL_BASIS[locale],
aiDefinitionCriteria: AI_DEFINITION_CRITERIA.map((i) => resolveItem(i, locale)),
prohibitedPractices: PROHIBITED_PRACTICES.map((i) => resolveItem(i, locale)),
annexIIICategories: ANNEX_III_CATEGORIES.map((i) => resolveItem(i, locale)),
transparencyTriggers: TRANSPARENCY_TRIGGERS.map((i) => resolveItem(i, locale)),
obligations: OBLIGATIONS.map((o) => resolveObligation(o, locale)),
deadlines: DEADLINES.map((d) => resolveDeadline(d, locale)),
national: NATIONAL_PACKS.map((n) => resolveNational(n, locale)),
};
}
/** Deutscher Content-Pack (Kompatibilitäts-Export, u. a. für Tests). */
export const CONTENT_PACK: ContentPack = getContentPack('de');

View File

@ -0,0 +1,260 @@
import { lt } from '../i18n/types';
import type { ContentItemSrc, DeadlineSrc } from './sourceTypes';
/** Kriterien der KI-Definition (Art. 3 Nr. 1). */
export const AI_DEFINITION_CRITERIA: readonly ContentItemSrc[] = [
{
id: 'def-autonomy',
label: lt(
'Das System arbeitet mit einem gewissen Grad an Autonomie (nicht rein regelbasiert deterministisch durch den Nutzer gesteuert).',
'The system operates with a degree of autonomy (not purely rule-based and deterministically controlled by the user).',
),
legalRef: 'Art. 3 Nr. 1',
},
{
id: 'def-inference',
label: lt(
'Das System leitet aus Eingaben ab, wie Ausgaben (Vorhersagen, Inhalte, Empfehlungen, Entscheidungen) erzeugt werden.',
'The system infers from inputs how to generate outputs (predictions, content, recommendations, decisions).',
),
legalRef: 'Art. 3 Nr. 1',
},
{
id: 'def-influence',
label: lt(
'Die Ausgaben können physische oder virtuelle Umgebungen beeinflussen.',
'The outputs can influence physical or virtual environments.',
),
legalRef: 'Art. 3 Nr. 1',
},
];
/** Verbotene Praktiken (Art. 5). */
export const PROHIBITED_PRACTICES: readonly ContentItemSrc[] = [
{
id: 'proh-manipulation',
label: lt(
'Unterschwellige oder gezielt manipulative Techniken, die Verhalten wesentlich verändern und erheblichen Schaden verursachen (können).',
'Subliminal or purposefully manipulative techniques that materially distort behaviour and cause (or are likely to cause) significant harm.',
),
legalRef: 'Art. 5 Abs. 1 lit. a',
},
{
id: 'proh-vulnerability',
label: lt(
'Ausnutzung von Schwächen aufgrund von Alter, Behinderung oder sozialer/wirtschaftlicher Lage mit erheblichem Schadensrisiko.',
'Exploitation of vulnerabilities due to age, disability or social/economic situation with a significant risk of harm.',
),
legalRef: 'Art. 5 Abs. 1 lit. b',
},
{
id: 'proh-social-scoring',
label: lt(
'Social Scoring: Bewertung von Personen anhand ihres Sozialverhaltens mit ungerechtfertigter Schlechterstellung.',
'Social scoring: evaluating persons based on social behaviour leading to unjustified detrimental treatment.',
),
legalRef: 'Art. 5 Abs. 1 lit. c',
},
{
id: 'proh-predictive-policing',
label: lt(
'Risikobewertung von Straftaten allein auf Basis von Profiling oder Persönlichkeitsmerkmalen.',
'Assessing the risk of criminal offences based solely on profiling or personality traits.',
),
legalRef: 'Art. 5 Abs. 1 lit. d',
},
{
id: 'proh-face-scraping',
label: lt(
'Ungezieltes Auslesen (Scraping) von Gesichtsbildern aus Internet oder Überwachungskameras zum Aufbau von Gesichtsdatenbanken.',
'Untargeted scraping of facial images from the internet or CCTV to build facial recognition databases.',
),
legalRef: 'Art. 5 Abs. 1 lit. e',
},
{
id: 'proh-emotion-workplace',
label: lt(
'Emotionserkennung am Arbeitsplatz oder in Bildungseinrichtungen (außer aus medizinischen oder Sicherheitsgründen).',
'Emotion recognition in the workplace or educational institutions (except for medical or safety reasons).',
),
legalRef: 'Art. 5 Abs. 1 lit. f',
},
{
id: 'proh-biometric-categorisation',
label: lt(
'Biometrische Kategorisierung zur Ableitung sensibler Merkmale (z. B. Ethnie, politische Meinung, sexuelle Orientierung).',
'Biometric categorisation to infer sensitive attributes (e.g. ethnicity, political opinion, sexual orientation).',
),
legalRef: 'Art. 5 Abs. 1 lit. g',
},
{
id: 'proh-rbi',
label: lt(
'Biometrische Echtzeit-Fernidentifizierung in öffentlich zugänglichen Räumen zu Strafverfolgungszwecken (enge Ausnahmen).',
'Real-time remote biometric identification in publicly accessible spaces for law enforcement (narrow exceptions).',
),
legalRef: 'Art. 5 Abs. 1 lit. h',
},
];
/** Hochrisiko-Bereiche nach Annex III. */
export const ANNEX_III_CATEGORIES: readonly ContentItemSrc[] = [
{
id: 'a3-biometrics',
label: lt(
'Biometrie: Fernidentifizierung, biometrische Kategorisierung, Emotionserkennung (soweit nicht verboten).',
'Biometrics: remote identification, biometric categorisation, emotion recognition (where not prohibited).',
),
legalRef: 'Annex III Nr. 1',
},
{
id: 'a3-critical-infra',
label: lt(
'Sicherheitskomponenten kritischer Infrastruktur (Verkehr, Wasser, Gas, Wärme, Strom, digitale Infrastruktur).',
'Safety components of critical infrastructure (traffic, water, gas, heating, electricity, digital infrastructure).',
),
legalRef: 'Annex III Nr. 2',
},
{
id: 'a3-education',
label: lt(
'Bildung: Zulassung, Bewertung von Lernergebnissen, Prüfungsüberwachung.',
'Education: admission, evaluation of learning outcomes, exam proctoring.',
),
legalRef: 'Annex III Nr. 3',
},
{
id: 'a3-employment',
label: lt(
'Beschäftigung: Recruiting/Bewerberauswahl, Beförderung/Kündigung, Aufgabenzuweisung, Leistungsüberwachung.',
'Employment: recruiting/candidate selection, promotion/termination, task allocation, performance monitoring.',
),
legalRef: 'Annex III Nr. 4',
hint: lt(
'Häufigster KMU-Fall: KI-gestütztes Bewerber-Screening.',
'Most common SME case: AI-supported applicant screening.',
),
},
{
id: 'a3-essential-services',
label: lt(
'Wesentliche Dienste: Kreditwürdigkeitsprüfung, Risiko-Pricing bei Lebens-/Krankenversicherung, Notruf-Triage, Sozialleistungen.',
'Essential services: credit scoring, risk pricing in life/health insurance, emergency call triage, public benefits.',
),
legalRef: 'Annex III Nr. 5',
},
{
id: 'a3-law-enforcement',
label: lt(
'Strafverfolgung (Beweismittelbewertung, Rückfallrisiko u. a.).',
'Law enforcement (evidence evaluation, recidivism risk, etc.).',
),
legalRef: 'Annex III Nr. 6',
},
{
id: 'a3-migration',
label: lt('Migration, Asyl, Grenzkontrolle.', 'Migration, asylum, border control.'),
legalRef: 'Annex III Nr. 7',
},
{
id: 'a3-justice',
label: lt(
'Rechtspflege und demokratische Prozesse (Unterstützung richterlicher Entscheidungen, Wahlbeeinflussung).',
'Administration of justice and democratic processes (supporting judicial decisions, influencing elections).',
),
legalRef: 'Annex III Nr. 8',
},
];
/** Auslöser der Transparenzpflichten (Art. 50). */
export const TRANSPARENCY_TRIGGERS: readonly ContentItemSrc[] = [
{
id: 'tr-chatbot',
label: lt(
'Das System interagiert direkt mit Menschen (z. B. Chatbot, Voicebot).',
'The system interacts directly with humans (e.g. chatbot, voicebot).',
),
legalRef: 'Art. 50 Abs. 1',
hint: lt(
'Nutzer müssen erkennen können, dass sie mit KI interagieren.',
'Users must be able to recognise that they are interacting with AI.',
),
},
{
id: 'tr-synthetic-content',
label: lt(
'Das System erzeugt synthetische Audio-, Bild-, Video- oder Textinhalte.',
'The system generates synthetic audio, image, video or text content.',
),
legalRef: 'Art. 50 Abs. 2',
hint: lt(
'Maschinenlesbare Kennzeichnung der Ausgaben erforderlich.',
'Machine-readable marking of outputs is required.',
),
},
{
id: 'tr-emotion',
label: lt(
'Emotionserkennung oder biometrische Kategorisierung wird eingesetzt (soweit erlaubt).',
'Emotion recognition or biometric categorisation is used (where permitted).',
),
legalRef: 'Art. 50 Abs. 3',
},
{
id: 'tr-deepfake',
label: lt(
'Das System erzeugt oder manipuliert Deepfakes (Bild/Audio/Video realer Personen, Orte oder Ereignisse).',
'The system generates or manipulates deepfakes (image/audio/video of real persons, places or events).',
),
legalRef: 'Art. 50 Abs. 4',
},
];
/** Geltungsfristen des AI Act. */
export const DEADLINES: readonly DeadlineSrc[] = [
{
id: 'dl-prohibitions',
date: '2025-02-02',
label: lt('Verbote & KI-Kompetenz', 'Prohibitions & AI literacy'),
description: lt(
'Verbotene Praktiken (Art. 5) und KI-Kompetenzpflicht (Art. 4) gelten. Bereits in Kraft.',
'Prohibited practices (Art. 5) and AI literacy duty (Art. 4) apply. Already in force.',
),
},
{
id: 'dl-gpai',
date: '2025-08-02',
label: lt('GPAI-Pflichten', 'GPAI obligations'),
description: lt(
'Pflichten für Anbieter von GPAI-Modellen (Art. 53 ff.) gelten. Bereits in Kraft.',
'Obligations for providers of GPAI models (Art. 53 et seq.) apply. Already in force.',
),
},
{
id: 'dl-general',
date: '2026-08-02',
label: lt('Allgemeine Geltung', 'General application'),
description: lt(
'Transparenzpflichten (Art. 50), Sanktionsregime und Governance-Struktur gelten.',
'Transparency obligations (Art. 50), penalty regime and governance structure apply.',
),
},
{
id: 'dl-annex3',
date: '2027-12-02',
label: lt('Hochrisiko (Annex III)', 'High-risk (Annex III)'),
description: lt(
'Pflichten für Annex-III-Hochrisiko-Systeme gelten (durch Digital Omnibus von Aug 2026 verschoben).',
'Obligations for Annex III high-risk systems apply (postponed from Aug 2026 by the Digital Omnibus).',
),
},
{
id: 'dl-annex1',
date: '2028-08-02',
label: lt('Hochrisiko (Annex I)', 'High-risk (Annex I)'),
description: lt(
'Pflichten für KI als Sicherheitsbauteil regulierter Produkte (Annex I) gelten.',
'Obligations for AI as a safety component of regulated products (Annex I) apply.',
),
},
];

View File

@ -0,0 +1,237 @@
import { lt } from '../i18n/types';
import type { NationalPackSrc } from './sourceTypes';
/**
* Nationale Zusatzanforderungen der EU-Mitgliedstaaten (Stand Juli 2026).
*
* Der AI Act ist eine Verordnung und gilt unmittelbar die Mitgliedstaaten
* regeln aber Aufsicht, Sanktionen und teils zusätzliche Pflichten national.
* Diese Module sind zuschaltbar und werden mit dem Content-Pack gepflegt.
*/
export const NATIONAL_PACKS: readonly NationalPackSrc[] = [
{
code: 'DE',
country: lt('Deutschland', 'Germany'),
authority: lt(
'Bundesnetzagentur (BNetzA) — Marktüberwachung, KI-Marktüberwachungskammer (UKIM), KoKIVO-Service-Desk',
'Federal Network Agency (BNetzA) — market surveillance, AI Market Surveillance Chamber (UKIM), KoKIVO service desk',
),
items: [
{
id: 'de-kimig-authority',
title: lt(
'Zuständigkeit und Meldewege nach KI-MIG kennen',
'Know competences and reporting channels under the German AI Act implementation law',
),
articles: 'KI-MIG (2026)',
description: lt(
'Das deutsche Durchführungsgesetz benennt die Bundesnetzagentur als zentrale Marktüberwachungs- und Anlaufstelle (KoKIVO als Service-Desk). Meldungen (z. B. Art. 73) laufen über die BNetzA; sektorale Behörden (z. B. BaFin) bleiben teils zuständig.',
'The German implementation law designates the Federal Network Agency as central market surveillance body and contact point (KoKIVO service desk). Reports (e.g. Art. 73) go to BNetzA; sectoral authorities (e.g. BaFin) partly remain competent.',
),
deadline: '2026-08-02',
roles: ['provider', 'deployer', 'importer', 'distributor'],
riskClasses: ['high', 'limited'],
},
{
id: 'de-betrvg',
title: lt(
'Mitbestimmung des Betriebsrats bei KI am Arbeitsplatz',
'Works council co-determination for AI at the workplace',
),
articles: '§ 87 Abs. 1 Nr. 6, § 90 BetrVG',
description: lt(
'Kann das System Verhalten oder Leistung von Beschäftigten überwachen, besteht ein Mitbestimmungsrecht des Betriebsrats; bei Planung von KI-Einsatz ist der Betriebsrat frühzeitig zu unterrichten (§ 90). Betriebsvereinbarung empfohlen.',
'If the system can monitor employee behaviour or performance, the works council has a co-determination right; it must be informed early when AI use is planned (Sec. 90). A works agreement is recommended.',
),
deadline: '2025-02-02',
roles: ['deployer'],
riskClasses: ['high', 'limited'],
},
{
id: 'de-dsgvo-bdsg',
title: lt(
'DSGVO/BDSG-Konformität der KI-Datenverarbeitung sicherstellen',
'Ensure GDPR/BDSG conformity of AI data processing',
),
articles: 'DSGVO, BDSG',
description: lt(
'Rechtsgrundlage der Verarbeitung, Informationspflichten, Betroffenenrechte und ggf. § 26 BDSG (Beschäftigtendaten) prüfen; Datenschutzbeauftragten einbinden.',
'Check legal basis of processing, information duties, data subject rights and, where relevant, Sec. 26 BDSG (employee data); involve the data protection officer.',
),
deadline: '2025-02-02',
roles: ['provider', 'deployer'],
riskClasses: ['high', 'limited', 'minimal'],
},
],
},
{
code: 'AT',
country: lt('Österreich', 'Austria'),
authority: lt(
'KI-Servicestelle bei der RTR (Rundfunk und Telekom Regulierungs-GmbH); Marktüberwachungsstruktur in Umsetzung',
'AI service desk at RTR (Austrian Regulatory Authority for Broadcasting and Telecommunications); market surveillance structure being implemented',
),
items: [
{
id: 'at-servicestelle',
title: lt(
'KI-Servicestelle als Anlaufstelle nutzen, nationale Umsetzung beobachten',
'Use the AI service desk as contact point, monitor national implementation',
),
articles: 'RTR KI-Servicestelle',
description: lt(
'Österreich hat bei der RTR eine KI-Servicestelle für Unternehmen eingerichtet; das nationale Begleitgesetz mit endgültiger Behördenstruktur und Sanktionen ist zu beobachten.',
'Austria has set up an AI service desk at RTR for companies; the national implementing act with the final authority structure and penalties should be monitored.',
),
deadline: '2026-08-02',
roles: ['provider', 'deployer'],
riskClasses: ['high', 'limited'],
},
],
},
{
code: 'IT',
country: lt('Italien', 'Italy'),
authority: lt(
'AgID (Agentur für digitales Italien) und ACN (Nationale Cybersicherheitsagentur)',
'AgID (Agency for Digital Italy) and ACN (National Cybersecurity Agency)',
),
items: [
{
id: 'it-l132-workers',
title: lt(
'Beschäftigte über KI-Einsatz informieren (Legge 132/2025)',
'Inform employees about AI use (Law 132/2025)',
),
articles: 'Legge 132/2025',
description: lt(
'Italiens nationales KI-Gesetz verpflichtet Arbeitgeber, Beschäftigte über den Einsatz von KI in Entscheidungsprozessen zu informieren — unabhängig von der Risikoklasse des Systems.',
'Italys national AI law obliges employers to inform employees about the use of AI in decision-making processes — regardless of the systems risk class.',
),
deadline: '2025-10-10',
roles: ['deployer'],
riskClasses: ['high', 'limited', 'minimal'],
},
{
id: 'it-l132-deepfake',
title: lt(
'Strafbarkeit rechtswidriger Deepfakes beachten',
'Note criminal liability for unlawful deepfakes',
),
articles: 'Legge 132/2025',
description: lt(
'Die rechtswidrige Verbreitung schädigender Deepfakes ist in Italien strafbewehrt (bis zu 5 Jahre Freiheitsstrafe). Kennzeichnungs- und Freigabeprozesse für generierte Inhalte einrichten.',
'Unlawful dissemination of harmful deepfakes is a criminal offence in Italy (up to 5 years imprisonment). Set up labelling and approval processes for generated content.',
),
deadline: '2025-10-10',
roles: ['provider', 'deployer'],
riskClasses: ['high', 'limited'],
},
{
id: 'it-l132-sector',
title: lt(
'Sektorregeln (Gesundheit, Justiz, Arbeit) und Durchführungsdekrete beobachten',
'Monitor sector rules (health, justice, labour) and implementing decrees',
),
articles: 'Legge 132/2025',
description: lt(
'Das Gesetz enthält Sonderregeln u. a. für Gesundheitswesen und Justiz; Durchführungsdekrete (inkl. Sanktionsregime) werden bis Ende 2026 erwartet.',
'The law contains special rules for healthcare and justice, among others; implementing decrees (incl. penalty regime) are expected by the end of 2026.',
),
deadline: '2026-10-10',
roles: ['provider', 'deployer'],
riskClasses: ['high'],
},
],
},
{
code: 'ES',
country: lt('Spanien', 'Spain'),
authority: lt(
'AESIA (Agencia Española de Supervisión de la Inteligencia Artificial)',
'AESIA (Spanish Agency for the Supervision of Artificial Intelligence)',
),
items: [
{
id: 'es-aesia',
title: lt(
'AESIA-Aufsicht und spanisches Sanktionsgesetz beachten',
'Note AESIA supervision and the Spanish sanctions bill',
),
articles: 'AESIA; Anteproyecto de Ley de gobernanza de la IA',
description: lt(
'Spanien hat mit AESIA als erstes EU-Land eine eigene KI-Aufsichtsbehörde; ein nationales Gesetz zu Sanktionen und Governance ist im Verfahren. AESIA ist zentrale Anlauf- und Meldestelle.',
'Spain was the first EU country with a dedicated AI supervisory agency (AESIA); a national law on sanctions and governance is in the legislative process. AESIA is the central contact and reporting point.',
),
deadline: '2026-08-02',
roles: ['provider', 'deployer', 'importer', 'distributor'],
riskClasses: ['high', 'limited'],
},
{
id: 'es-sandbox',
title: lt(
'Regulatorischen Sandbox-Zugang prüfen (RD 817/2023)',
'Consider regulatory sandbox access (RD 817/2023)',
),
articles: 'Real Decreto 817/2023',
description: lt(
'Spanien betreibt eine KI-Regulierungs-Sandbox für Hochrisiko-Systeme — für Anbieter eine Möglichkeit, Konformität unter Behördenbegleitung zu erproben.',
'Spain operates an AI regulatory sandbox for high-risk systems — an opportunity for providers to test conformity under regulatory guidance.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
],
},
{
code: 'FR',
country: lt('Frankreich', 'France'),
authority: lt(
'Mehrbehördenmodell; CNIL federführend für KI mit Personenbezug, DGCCRF u. a. für Marktüberwachung',
'Multi-authority model; CNIL leads on AI involving personal data, DGCCRF and others for market surveillance',
),
items: [
{
id: 'fr-cnil',
title: lt(
'CNIL-Empfehlungen zu KI und DSGVO umsetzen',
'Implement CNIL recommendations on AI and GDPR',
),
articles: 'CNIL — Recommandations IA',
description: lt(
'Die CNIL hat detaillierte Leitlinien zur DSGVO-konformen Entwicklung und Nutzung von KI veröffentlicht (Rechtsgrundlagen, Trainingsdaten, Betroffenenrechte); bei Personenbezug maßgeblich.',
'The CNIL has published detailed guidance on GDPR-compliant development and use of AI (legal bases, training data, data subject rights); authoritative where personal data is involved.',
),
deadline: '2025-02-02',
roles: ['provider', 'deployer'],
riskClasses: ['high', 'limited', 'minimal'],
},
],
},
{
code: 'NL',
country: lt('Niederlande', 'Netherlands'),
authority: lt(
'Autoriteit Persoonsgegevens (AP) — Koordination Algorithmen-Aufsicht (DCA); RDI für technische Marktüberwachung',
'Dutch Data Protection Authority (AP) — algorithm oversight coordination (DCA); RDI for technical market surveillance',
),
items: [
{
id: 'nl-algo-register',
title: lt(
'Algorithmenregister für öffentliche Stellen pflegen',
'Maintain the algorithm register for public bodies',
),
articles: 'Algoritmeregister (verplicht für Behörden)',
description: lt(
'Niederländische Behörden müssen eingesetzte (wirkungsrelevante) Algorithmen und KI-Systeme im öffentlichen Algorithmenregister dokumentieren; private Auftragnehmer öffentlicher Stellen sind mittelbar betroffen.',
'Dutch public authorities must document deployed (impact-relevant) algorithms and AI systems in the public algorithm register; private contractors of public bodies are indirectly affected.',
),
deadline: '2025-02-02',
roles: ['deployer'],
riskClasses: ['high', 'limited'],
},
],
},
];

View File

@ -0,0 +1,366 @@
import { lt } from '../i18n/types';
import type { ObligationSrc } from './sourceTypes';
/** EU-weite Pflichten aus dem AI Act (rollen- und risikoklassenspezifisch). */
export const OBLIGATIONS: readonly ObligationSrc[] = [
// ── Alle Risikoklassen / alle Rollen ──────────────────────────────
{
id: 'ob-ai-literacy',
title: lt('KI-Kompetenz des Personals sicherstellen', 'Ensure AI literacy of staff'),
articles: 'Art. 4',
description: lt(
'Mitarbeitende, die KI-Systeme betreiben oder nutzen, müssen über ausreichende KI-Kompetenz verfügen (Schulungen, Richtlinien). Gilt seit 2. Februar 2025 für alle Anbieter und Betreiber.',
'Staff operating or using AI systems must have sufficient AI literacy (training, policies). Applies since 2 February 2025 to all providers and deployers.',
),
deadline: '2025-02-02',
roles: ['provider', 'deployer', 'importer', 'distributor'],
riskClasses: ['high', 'limited', 'minimal'],
},
// ── Hochrisiko: Anbieter ──────────────────────────────────────────
{
id: 'ob-risk-mgmt',
title: lt('Risikomanagementsystem einrichten', 'Establish a risk management system'),
articles: 'Art. 9',
description: lt(
'Kontinuierlicher, iterativer Risikomanagementprozess über den gesamten Lebenszyklus: Risiken identifizieren, bewerten, mindern, testen.',
'Continuous, iterative risk management process across the entire lifecycle: identify, assess, mitigate and test risks.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-data-governance',
title: lt(
'Daten-Governance für Trainings-/Test-/Validierungsdaten',
'Data governance for training/testing/validation data',
),
articles: 'Art. 10',
description: lt(
'Datensätze müssen relevant, repräsentativ, möglichst fehlerfrei und vollständig sein; Bias-Prüfung und geeignete Erhebungsverfahren dokumentieren.',
'Datasets must be relevant, representative, as error-free and complete as possible; document bias examination and appropriate collection procedures.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-tech-doc',
title: lt('Technische Dokumentation (Annex IV) erstellen', 'Prepare technical documentation (Annex IV)'),
articles: 'Art. 11, Annex IV',
description: lt(
'Vollständige technische Dokumentation vor Inverkehrbringen; für KMU gelten vereinfachte Formulare. Der Dokumenten-Generator dieses Tools erzeugt das Grundgerüst.',
'Complete technical documentation before placing on the market; simplified forms apply to SMEs. This tool generates the skeleton.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-logging',
title: lt('Protokollierung (Logging) technisch ermöglichen', 'Enable logging technically'),
articles: 'Art. 12',
description: lt(
'Automatische Aufzeichnung von Ereignissen über den Lebenszyklus, um Rückverfolgbarkeit und Marktüberwachung zu ermöglichen.',
'Automatic recording of events across the lifecycle to enable traceability and market surveillance.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-transparency-instructions',
title: lt(
'Betriebsanleitung und Transparenzinformationen bereitstellen',
'Provide instructions for use and transparency information',
),
articles: 'Art. 13',
description: lt(
'Klare Gebrauchsanweisung: Zweckbestimmung, Genauigkeit, Grenzen, menschliche Aufsicht, erwartete Lebensdauer und Wartung.',
'Clear instructions for use: intended purpose, accuracy, limitations, human oversight, expected lifetime and maintenance.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-human-oversight',
title: lt('Menschliche Aufsicht konzipieren', 'Design human oversight'),
articles: 'Art. 14',
description: lt(
'Das System muss wirksam durch Menschen beaufsichtigt werden können (Eingriffs-/Abbruchmöglichkeiten, Automation-Bias-Vorkehrungen).',
'The system must allow effective human oversight (intervention/stop mechanisms, safeguards against automation bias).',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-accuracy-robustness',
title: lt(
'Genauigkeit, Robustheit und Cybersicherheit gewährleisten',
'Ensure accuracy, robustness and cybersecurity',
),
articles: 'Art. 15',
description: lt(
'Angemessenes Niveau an Genauigkeit und Widerstandsfähigkeit, inkl. Schutz gegen KI-spezifische Angriffe (Data Poisoning, Adversarial Examples, Prompt Injection).',
'Appropriate level of accuracy and resilience, incl. protection against AI-specific attacks (data poisoning, adversarial examples, prompt injection).',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-qms',
title: lt('Qualitätsmanagementsystem einführen', 'Implement a quality management system'),
articles: 'Art. 17',
description: lt(
'Dokumentiertes QMS: Compliance-Strategie, Prüfverfahren, Datenmanagement, Risikomanagement, Meldeverfahren, Verantwortlichkeiten.',
'Documented QMS: compliance strategy, testing procedures, data management, risk management, reporting procedures, responsibilities.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-corrective',
title: lt(
'Korrekturmaßnahmen und Informationspflichten organisieren',
'Organise corrective actions and information duties',
),
articles: 'Art. 20, 21',
description: lt(
'Bei Nichtkonformität: unverzüglich korrigieren, zurücknehmen oder zurückrufen; Händler, Betreiber und Behörden informieren; mit zuständigen Behörden kooperieren.',
'In case of non-conformity: immediately correct, withdraw or recall; inform distributors, deployers and authorities; cooperate with competent authorities.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-authorized-rep',
title: lt(
'Bevollmächtigten in der EU benennen (Nicht-EU-Anbieter)',
'Appoint an authorised representative in the EU (non-EU providers)',
),
articles: 'Art. 22',
description: lt(
'Anbieter mit Sitz außerhalb der EU müssen vor Bereitstellung einen in der EU niedergelassenen Bevollmächtigten schriftlich mandatieren.',
'Providers established outside the EU must appoint, by written mandate, an authorised representative established in the EU before making systems available.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-conformity',
title: lt(
'Konformitätsbewertung durchführen, CE-Kennzeichnung, EU-Datenbank-Registrierung',
'Conduct conformity assessment, CE marking, EU database registration',
),
articles: 'Art. 43, 4749',
description: lt(
'Konformitätsbewertungsverfahren abschließen, EU-Konformitätserklärung ausstellen, CE-Kennzeichnung anbringen und System in der EU-Datenbank registrieren.',
'Complete the conformity assessment procedure, issue the EU declaration of conformity, affix CE marking and register the system in the EU database.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-postmarket',
title: lt(
'Plan zur Beobachtung nach dem Inverkehrbringen aufstellen',
'Establish a post-market monitoring plan',
),
articles: 'Art. 72',
description: lt(
'Systematische Sammlung und Auswertung von Betriebsdaten über die gesamte Lebensdauer, um fortlaufende Konformität sicherzustellen; Teil der technischen Dokumentation.',
'Systematically collect and analyse operational data over the entire lifetime to ensure continued conformity; part of the technical documentation.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
{
id: 'ob-incident',
title: lt(
'Meldeprozess für schwerwiegende Vorfälle einrichten',
'Set up a serious-incident reporting process',
),
articles: 'Art. 73',
description: lt(
'Schwerwiegende Vorfälle sind der Marktüberwachungsbehörde zu melden (grundsätzlich binnen 15 Tagen; bei Todesfällen/weitverbreiteten Verstößen kürzere Fristen). Interner Melde- und Untersuchungsprozess erforderlich.',
'Serious incidents must be reported to the market surveillance authority (generally within 15 days; shorter deadlines for deaths/widespread infringements). An internal reporting and investigation process is required.',
),
deadline: '2027-12-02',
roles: ['provider'],
riskClasses: ['high'],
},
// ── Hochrisiko: Betreiber ─────────────────────────────────────────
{
id: 'ob-deployer-use',
title: lt('Nutzung gemäß Betriebsanleitung sicherstellen', 'Ensure use according to instructions'),
articles: 'Art. 26 Abs. 1',
description: lt(
'Technische und organisatorische Maßnahmen, damit das System entsprechend der Gebrauchsanweisung des Anbieters genutzt wird.',
'Technical and organisational measures to ensure the system is used in accordance with the providers instructions for use.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-oversight',
title: lt('Kompetente menschliche Aufsicht zuweisen', 'Assign competent human oversight'),
articles: 'Art. 26 Abs. 2',
description: lt(
'Aufsicht durch Personen mit erforderlicher Kompetenz, Ausbildung und Befugnis.',
'Oversight by persons with the necessary competence, training and authority.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-input',
title: lt(
'Relevanz und Repräsentativität der Eingabedaten sicherstellen',
'Ensure relevance and representativeness of input data',
),
articles: 'Art. 26 Abs. 4',
description: lt(
'Soweit der Betreiber die Eingabedaten kontrolliert, müssen diese für die Zweckbestimmung relevant und hinreichend repräsentativ sein.',
'Insofar as the deployer controls the input data, it must be relevant and sufficiently representative for the intended purpose.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-monitoring',
title: lt(
'Betrieb überwachen, Vorfälle melden, Logs aufbewahren',
'Monitor operation, report incidents, retain logs',
),
articles: 'Art. 26 Abs. 56',
description: lt(
'Betrieb anhand der Anleitung überwachen; bei Risiken Anbieter/Behörden informieren; automatisch erzeugte Logs mind. 6 Monate aufbewahren.',
'Monitor operation based on the instructions; inform provider/authorities of risks; retain automatically generated logs for at least 6 months.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-deployer-workers',
title: lt(
'Beschäftigte vor Einsatz am Arbeitsplatz informieren',
'Inform employees before workplace use',
),
articles: 'Art. 26 Abs. 7',
description: lt(
'Vor Inbetriebnahme eines Hochrisiko-Systems am Arbeitsplatz sind Arbeitnehmervertreter und betroffene Beschäftigte zu informieren.',
'Before putting a high-risk system into service at the workplace, workers representatives and affected employees must be informed.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-registration-deployer',
title: lt(
'Registrierung in der EU-Datenbank prüfen (öffentliche Stellen)',
'Check EU database registration (public bodies)',
),
articles: 'Art. 49 Abs. 3',
description: lt(
'Betreiber, die Behörden oder öffentliche Einrichtungen sind (oder in deren Auftrag handeln), müssen sich vor Nutzung eines Annex-III-Systems in der EU-Datenbank registrieren.',
'Deployers that are public authorities or bodies (or act on their behalf) must register in the EU database before using an Annex III system.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-dsgvo-dpia',
title: lt(
'Datenschutz-Folgenabschätzung (DSFA) durchführen',
'Carry out a data protection impact assessment (DPIA)',
),
articles: 'DSGVO Art. 35 i. V. m. AI Act Art. 26 Abs. 9',
description: lt(
'Verarbeitet das Hochrisiko-System personenbezogene Daten, ist regelmäßig eine DSFA nach DSGVO erforderlich; die Anbieter-Informationen nach Art. 13 AI Act sind dafür zu nutzen.',
'If the high-risk system processes personal data, a DPIA under the GDPR is regularly required; use the provider information under Art. 13 AI Act for this purpose.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
{
id: 'ob-fria',
title: lt(
'Grundrechte-Folgenabschätzung (FRIA) prüfen',
'Assess fundamental rights impact (FRIA)',
),
articles: 'Art. 27',
description: lt(
'Pflicht für öffentliche Stellen und private Anbieter öffentlicher Dienste sowie bestimmte Annex-III-Fälle (z. B. Kreditwürdigkeit, Versicherung): Folgen für Grundrechte vor Einsatz bewerten.',
'Required for public bodies and private providers of public services as well as certain Annex III cases (e.g. creditworthiness, insurance): assess fundamental rights impact before use.',
),
deadline: '2027-12-02',
roles: ['deployer'],
riskClasses: ['high'],
},
// ── Importeur / Händler ───────────────────────────────────────────
{
id: 'ob-importer-verify',
title: lt('Konformität vor Inverkehrbringen prüfen', 'Verify conformity before placing on the market'),
articles: 'Art. 23',
description: lt(
'Importeure müssen prüfen: Konformitätsbewertung durchgeführt, technische Dokumentation vorhanden, CE-Kennzeichnung und Ansprechpartner benannt.',
'Importers must verify: conformity assessment performed, technical documentation available, CE marking affixed and contact person designated.',
),
deadline: '2027-12-02',
roles: ['importer'],
riskClasses: ['high'],
},
{
id: 'ob-distributor-verify',
title: lt('Sorgfaltspflichten der Vertriebskette erfüllen', 'Fulfil distribution chain due diligence'),
articles: 'Art. 24',
description: lt(
'Händler prüfen CE-Kennzeichnung, Konformitätserklärung und Gebrauchsanweisung; bei Zweifeln keine Bereitstellung.',
'Distributors check CE marking, declaration of conformity and instructions for use; do not make available in case of doubt.',
),
deadline: '2027-12-02',
roles: ['distributor'],
riskClasses: ['high'],
},
// ── Transparenzpflichten (limited) ────────────────────────────────
{
id: 'ob-transparency-art50',
title: lt('Transparenzpflichten nach Art. 50 umsetzen', 'Implement transparency obligations under Art. 50'),
articles: 'Art. 50',
description: lt(
'Je nach Auslöser: KI-Interaktion offenlegen (Chatbots), synthetische Inhalte maschinenlesbar kennzeichnen, Deepfakes sichtbar kennzeichnen, über Emotionserkennung informieren.',
'Depending on the trigger: disclose AI interaction (chatbots), machine-readably mark synthetic content, visibly label deepfakes, inform about emotion recognition.',
),
deadline: '2026-08-02',
roles: ['provider', 'deployer'],
riskClasses: ['high', 'limited'],
},
// ── GPAI ──────────────────────────────────────────────────────────
{
id: 'ob-gpai',
title: lt('GPAI-Modellpflichten erfüllen', 'Fulfil GPAI model obligations'),
articles: 'Art. 5355',
description: lt(
'Für Anbieter von GPAI-Modellen: technische Modell-Dokumentation, Informationen für nachgelagerte Anbieter, Urheberrechts-Policy, Zusammenfassung der Trainingsdaten. Bei systemischem Risiko zusätzlich Evaluierungen und Incident-Meldungen.',
'For providers of GPAI models: technical model documentation, information for downstream providers, copyright policy, training data summary. For systemic risk additionally evaluations and incident reports.',
),
deadline: '2025-08-02',
roles: ['provider'],
riskClasses: ['high', 'limited', 'minimal'],
},
];

View File

@ -0,0 +1,37 @@
import type { RiskClass, Role } from '../engine/types';
import type { LText } from '../i18n/types';
/** Checklisten-Item in Quellform (lokalisiert). */
export interface ContentItemSrc {
readonly id: string;
readonly label: LText;
readonly legalRef: string;
readonly hint?: LText;
}
/** Pflicht in Quellform (lokalisiert). */
export interface ObligationSrc {
readonly id: string;
readonly title: LText;
readonly articles: string;
readonly description: LText;
readonly deadline: string;
readonly roles: readonly Role[];
readonly riskClasses: readonly RiskClass[];
}
/** Geltungsfrist in Quellform (lokalisiert). */
export interface DeadlineSrc {
readonly id: string;
readonly date: string;
readonly label: LText;
readonly description: LText;
}
/** Nationales Modul in Quellform (lokalisiert). */
export interface NationalPackSrc {
readonly code: string;
readonly country: LText;
readonly authority: LText;
readonly items: readonly ObligationSrc[];
}

View File

@ -0,0 +1,77 @@
import { deriveObligations } from './obligations';
import type { AISystem, ContentPack, Obligation, ObligationStatus } from './types';
/** Ein Eintrag der systemübergreifenden Aufgabenliste. */
export interface ChecklistEntry {
readonly systemId: string;
readonly systemName: string;
readonly obligation: Obligation;
readonly status: ObligationStatus;
/** Erledigt oder als nicht anwendbar markiert. */
readonly resolved: boolean;
/** Frist liegt am oder vor dem Stichtag. */
readonly due: boolean;
}
/** Gruppierte Aufgabenliste mit Fortschritt. */
export interface Checklist {
/** Fällig (Frist erreicht/überschritten) und noch nicht erledigt. */
readonly dueNow: readonly ChecklistEntry[];
/** Kommt auf das Unternehmen zu (Frist in der Zukunft), noch offen. */
readonly upcoming: readonly ChecklistEntry[];
/** Erledigt oder nicht anwendbar. */
readonly resolved: readonly ChecklistEntry[];
readonly total: number;
readonly resolvedCount: number;
}
/**
* Baut die systemübergreifende, abhakbare Aufgabenliste.
*
* Jede Pflicht jedes Systems wird zu einem Eintrag; Sortierung innerhalb
* der Gruppen nach Frist aufsteigend, dann Systemname.
*
* @param systems - Alle registrierten KI-Systeme
* @param pack - Regulatorischer Content-Pack (Sprache des Aufrufers)
* @param countries - Aktivierte Länder-Module
* @param today - Stichtag im ISO-Format (YYYY-MM-DD)
*/
export function buildChecklist(
systems: readonly AISystem[],
pack: ContentPack,
countries: readonly string[],
today: string,
): Checklist {
const entries: ChecklistEntry[] = systems.flatMap((system) =>
deriveObligations(system.classification, system.answers, pack, countries).map(
(obligation) => {
const status = system.obligationStatus[obligation.id] ?? 'open';
const resolved = status === 'done' || status === 'not-applicable';
return {
systemId: system.id,
systemName: system.name,
obligation,
status,
resolved,
due: obligation.deadline <= today,
};
},
),
);
const byDeadline = (a: ChecklistEntry, b: ChecklistEntry): number =>
a.obligation.deadline.localeCompare(b.obligation.deadline) ||
a.systemName.localeCompare(b.systemName);
const dueNow = entries.filter((e) => !e.resolved && e.due).sort(byDeadline);
const upcoming = entries.filter((e) => !e.resolved && !e.due).sort(byDeadline);
const resolved = entries.filter((e) => e.resolved).sort(byDeadline);
return {
dueNow,
upcoming,
resolved,
total: entries.length,
resolvedCount: resolved.length,
};
}

View File

@ -1,5 +1,50 @@
import type { Locale } from '../i18n/types';
import type { AssessmentAnswers, Classification, ContentPack } from './types';
interface ClassifyStrings {
readonly outOfScope: string;
readonly prohibited: (label: string, ref: string) => string;
readonly annexI: string;
readonly annexIII: (label: string, ref: string) => string;
readonly profilingBar: string;
readonly art63: string;
readonly transparency: (label: string, ref: string) => string;
readonly minimal: string;
}
const STRINGS: Readonly<Record<Locale, ClassifyStrings>> = {
de: {
outOfScope:
'Das System erfüllt die KI-Definition nach Art. 3 Nr. 1 nicht — der AI Act ist nicht anwendbar.',
prohibited: (label, ref) => `Verbotene Praktik: ${label} (${ref})`,
annexI:
'Das System ist Sicherheitsbauteil eines in Annex I regulierten Produkts bzw. selbst ein solches Produkt (Art. 6 Abs. 1) — Hochrisiko.',
annexIII: (label, ref) => `Annex-III-Bereich: ${label} (${ref})`,
profilingBar:
'Das System führt Profiling natürlicher Personen durch — die Ausnahme nach Art. 6 Abs. 3 ist damit ausgeschlossen.',
art63:
'Ausnahme nach Art. 6 Abs. 3 in Anspruch genommen (eng begrenzte/vorbereitende Aufgabe ohne wesentliches Risiko). Die Einschätzung ist nach Art. 6 Abs. 4 zu dokumentieren.',
transparency: (label, ref) => `Transparenzpflicht: ${label} (${ref})`,
minimal:
'Keine verbotene Praktik, kein Hochrisiko-Bereich, keine Transparenzpflicht-Auslöser — minimales Risiko. Es gelten die allgemeine KI-Kompetenzpflicht (Art. 4) und freiwillige Verhaltenskodizes.',
},
en: {
outOfScope:
'The system does not meet the AI definition under Art. 3(1) — the AI Act does not apply.',
prohibited: (label, ref) => `Prohibited practice: ${label} (${ref})`,
annexI:
'The system is a safety component of a product regulated under Annex I, or such a product itself (Art. 6(1)) — high-risk.',
annexIII: (label, ref) => `Annex III area: ${label} (${ref})`,
profilingBar:
'The system performs profiling of natural persons — the exception under Art. 6(3) is therefore excluded.',
art63:
'Exception under Art. 6(3) claimed (narrow procedural/preparatory task without significant risk). The assessment must be documented under Art. 6(4).',
transparency: (label, ref) => `Transparency obligation: ${label} (${ref})`,
minimal:
'No prohibited practice, no high-risk area, no transparency triggers — minimal risk. The general AI literacy duty (Art. 4) and voluntary codes of conduct apply.',
},
};
/**
* Klassifiziert ein KI-System nach dem Risikomodell des EU AI Act.
*
@ -13,16 +58,22 @@ import type { AssessmentAnswers, Classification, ContentPack } from './types';
*
* @param answers - Antworten des Bewertungs-Wizards
* @param pack - Regulatorischer Content-Pack (für Begründungstexte)
* @param locale - Sprache der Begründungen (Default: Deutsch)
* @returns Unveränderliches Klassifizierungsergebnis mit Begründungen
*/
export function classify(answers: AssessmentAnswers, pack: ContentPack): Classification {
export function classify(
answers: AssessmentAnswers,
pack: ContentPack,
locale: Locale = 'de',
): Classification {
const s = STRINGS[locale];
const reasons: string[] = [];
const transparencyDuties = answers.transparencyTriggers.length > 0;
if (!answers.isAISystem) {
return {
riskClass: 'out-of-scope',
reasons: ['Das System erfüllt die KI-Definition nach Art. 3 Nr. 1 nicht — der AI Act ist nicht anwendbar.'],
reasons: [s.outOfScope],
gpaiObligations: false,
transparencyDuties: false,
art64DocumentationDuty: false,
@ -33,7 +84,7 @@ export function classify(answers: AssessmentAnswers, pack: ContentPack): Classif
for (const id of answers.prohibitedPractices) {
const item = pack.prohibitedPractices.find((p) => p.id === id);
if (item) {
reasons.push(`Verbotene Praktik: ${item.label} (${item.legalRef})`);
reasons.push(s.prohibited(item.label, item.legalRef));
}
}
return {
@ -46,9 +97,7 @@ export function classify(answers: AssessmentAnswers, pack: ContentPack): Classif
}
if (answers.isAnnexISafetyComponent) {
reasons.push(
'Das System ist Sicherheitsbauteil eines in Annex I regulierten Produkts bzw. selbst ein solches Produkt (Art. 6 Abs. 1) — Hochrisiko.',
);
reasons.push(s.annexI);
return {
riskClass: 'high',
reasons,
@ -62,14 +111,12 @@ export function classify(answers: AssessmentAnswers, pack: ContentPack): Classif
for (const id of answers.annexIIICategories) {
const item = pack.annexIIICategories.find((c) => c.id === id);
if (item) {
reasons.push(`Annex-III-Bereich: ${item.label} (${item.legalRef})`);
reasons.push(s.annexIII(item.label, item.legalRef));
}
}
if (answers.involvesProfiling) {
reasons.push(
'Das System führt Profiling natürlicher Personen durch — die Ausnahme nach Art. 6 Abs. 3 ist damit ausgeschlossen.',
);
reasons.push(s.profilingBar);
return {
riskClass: 'high',
reasons,
@ -80,13 +127,11 @@ export function classify(answers: AssessmentAnswers, pack: ContentPack): Classif
}
if (answers.art63Exception) {
reasons.push(
'Ausnahme nach Art. 6 Abs. 3 in Anspruch genommen (eng begrenzte/vorbereitende Aufgabe ohne wesentliches Risiko). Die Einschätzung ist nach Art. 6 Abs. 4 zu dokumentieren.',
);
reasons.push(s.art63);
return {
riskClass: transparencyDuties ? 'limited' : 'minimal',
reasons: transparencyDuties
? [...reasons, ...transparencyReasons(answers, pack)]
? [...reasons, ...transparencyReasons(answers, pack, s)]
: reasons,
gpaiObligations: answers.isGPAIModel,
transparencyDuties,
@ -106,7 +151,7 @@ export function classify(answers: AssessmentAnswers, pack: ContentPack): Classif
if (transparencyDuties) {
return {
riskClass: 'limited',
reasons: transparencyReasons(answers, pack),
reasons: transparencyReasons(answers, pack, s),
gpaiObligations: answers.isGPAIModel,
transparencyDuties: true,
art64DocumentationDuty: false,
@ -115,9 +160,7 @@ export function classify(answers: AssessmentAnswers, pack: ContentPack): Classif
return {
riskClass: 'minimal',
reasons: [
'Keine verbotene Praktik, kein Hochrisiko-Bereich, keine Transparenzpflicht-Auslöser — minimales Risiko. Es gelten die allgemeine KI-Kompetenzpflicht (Art. 4) und freiwillige Verhaltenskodizes.',
],
reasons: [s.minimal],
gpaiObligations: answers.isGPAIModel,
transparencyDuties: false,
art64DocumentationDuty: false,
@ -125,9 +168,13 @@ export function classify(answers: AssessmentAnswers, pack: ContentPack): Classif
}
/** Erzeugt Begründungstexte für zutreffende Art.-50-Auslöser. */
function transparencyReasons(answers: AssessmentAnswers, pack: ContentPack): string[] {
function transparencyReasons(
answers: AssessmentAnswers,
pack: ContentPack,
s: ClassifyStrings,
): string[] {
return answers.transparencyTriggers.flatMap((id) => {
const item = pack.transparencyTriggers.find((t) => t.id === id);
return item ? [`Transparenzpflicht: ${item.label} (${item.legalRef})`] : [];
return item ? [s.transparency(item.label, item.legalRef)] : [];
});
}

View File

@ -0,0 +1,201 @@
import type { Locale } from '../i18n/types';
import type { ObligationStatus, RiskClass, Role } from './types';
/** Anzeige-Labels und Textbausteine der Dokument-Generatoren je Sprache. */
export interface DocStrings {
readonly risk: Readonly<Record<RiskClass, string>>;
readonly role: Readonly<Record<Role, string>>;
readonly status: Readonly<Record<ObligationStatus, string>>;
readonly disclaimer: string;
readonly meta: {
readonly system: string;
readonly purpose: string;
readonly vendor: string;
readonly owner: string;
readonly role: string;
readonly riskClass: string;
readonly createdAt: string;
readonly contentPack: string;
readonly company: string;
readonly contact: string;
readonly contactPerson: string;
};
readonly report: {
readonly title: string;
readonly result: string;
readonly reasons: string;
readonly gpai: string;
readonly gpaiText: string;
readonly art64: string;
readonly art64Text: string;
};
readonly plan: {
readonly title: string;
readonly none: string;
readonly obligations: string;
readonly details: string;
readonly colObligation: string;
readonly colLegal: string;
readonly colDeadline: string;
readonly colStatus: string;
};
readonly inventory: {
readonly title: string;
readonly asOf: string;
readonly count: string;
readonly colSystem: string;
readonly colPurpose: string;
readonly colRole: string;
readonly colRisk: string;
readonly colOwner: string;
};
readonly todo: string;
}
const DE: DocStrings = {
risk: {
'out-of-scope': 'Nicht anwendbar (keine KI i. S. d. Art. 3 Nr. 1)',
prohibited: 'VERBOTEN (Art. 5)',
high: 'Hochrisiko',
limited: 'Begrenztes Risiko (Transparenzpflichten)',
minimal: 'Minimales Risiko',
},
role: {
provider: 'Anbieter (Provider)',
deployer: 'Betreiber (Deployer)',
importer: 'Einführer (Importer)',
distributor: 'Händler (Distributor)',
},
status: {
open: 'Offen',
'in-progress': 'In Umsetzung',
done: 'Erledigt',
'not-applicable': 'Nicht anwendbar',
},
disclaimer:
'> **Hinweis:** Dieses Dokument wurde mit dem AI-Act-Kompass erstellt und ist eine strukturierte Arbeitshilfe — **keine Rechtsberatung**. Die rechtliche Bewertung im Einzelfall obliegt dem Unternehmen bzw. seiner Rechtsberatung.',
meta: {
system: 'KI-System',
purpose: 'Zweckbestimmung',
vendor: 'Hersteller/Anbieter',
owner: 'Verantwortlich (intern)',
role: 'Rolle des Unternehmens',
riskClass: 'Risikoklasse',
createdAt: 'Erstellt am',
contentPack: 'Content-Pack',
company: 'Unternehmen',
contact: 'Kontakt',
contactPerson: 'Ansprechpartner',
},
report: {
title: 'Risikoklassifizierung nach EU AI Act',
result: 'Ergebnis',
reasons: 'Begründung',
gpai: 'GPAI',
gpaiText: 'Für dieses System sind zusätzlich GPAI-Modellpflichten (Art. 53 ff.) relevant.',
art64: 'Dokumentationspflicht Art. 6 Abs. 4',
art64Text:
'Die Inanspruchnahme der Ausnahme nach Art. 6 Abs. 3 wurde dokumentiert. Diese Einschätzung ist auf Verlangen der Marktüberwachung vorzulegen.',
},
plan: {
title: 'Maßnahmenplan EU AI Act',
none: 'Für dieses System bestehen keine umsetzbaren Pflichten (verboten oder außerhalb des Anwendungsbereichs).',
obligations: 'Pflichten',
details: 'Details',
colObligation: 'Pflicht',
colLegal: 'Rechtsgrundlage',
colDeadline: 'Frist',
colStatus: 'Status',
},
inventory: {
title: 'KI-Inventar (AI Asset Register)',
asOf: 'Stand',
count: 'Registrierte KI-Systeme',
colSystem: 'System',
colPurpose: 'Zweck',
colRole: 'Rolle',
colRisk: 'Risikoklasse',
colOwner: 'Verantwortlich',
},
todo: '_[AUSFÜLLEN]_',
};
const EN: DocStrings = {
risk: {
'out-of-scope': 'Not applicable (not AI within the meaning of Art. 3(1))',
prohibited: 'PROHIBITED (Art. 5)',
high: 'High-risk',
limited: 'Limited risk (transparency obligations)',
minimal: 'Minimal risk',
},
role: {
provider: 'Provider',
deployer: 'Deployer',
importer: 'Importer',
distributor: 'Distributor',
},
status: {
open: 'Open',
'in-progress': 'In progress',
done: 'Done',
'not-applicable': 'Not applicable',
},
disclaimer:
'> **Note:** This document was created with AI-Act-Kompass and is a structured working aid — **not legal advice**. The legal assessment of the individual case remains with the company and its legal counsel.',
meta: {
system: 'AI system',
purpose: 'Intended purpose',
vendor: 'Manufacturer/provider',
owner: 'Responsible (internal)',
role: 'Company role',
riskClass: 'Risk class',
createdAt: 'Created on',
contentPack: 'Content pack',
company: 'Company',
contact: 'Contact',
contactPerson: 'Contact person',
},
report: {
title: 'Risk classification under the EU AI Act',
result: 'Result',
reasons: 'Reasoning',
gpai: 'GPAI',
gpaiText: 'GPAI model obligations (Art. 53 et seq.) are additionally relevant for this system.',
art64: 'Documentation duty Art. 6(4)',
art64Text:
'The reliance on the exception under Art. 6(3) has been documented. This assessment must be provided to market surveillance authorities upon request.',
},
plan: {
title: 'EU AI Act action plan',
none: 'There are no implementable obligations for this system (prohibited or out of scope).',
obligations: 'Obligations',
details: 'Details',
colObligation: 'Obligation',
colLegal: 'Legal basis',
colDeadline: 'Deadline',
colStatus: 'Status',
},
inventory: {
title: 'AI inventory (AI asset register)',
asOf: 'As of',
count: 'Registered AI systems',
colSystem: 'System',
colPurpose: 'Purpose',
colRole: 'Role',
colRisk: 'Risk class',
colOwner: 'Responsible',
},
todo: '_[TO BE COMPLETED]_',
};
/** Liefert die Dokument-Strings für die gewünschte Sprache. */
export function docStrings(locale: Locale): DocStrings {
return locale === 'en' ? EN : DE;
}
/** Formatiert ein ISO-Datum landesüblich (DE: TT.MM.JJJJ, EN: ISO). */
export function formatDocDate(iso: string, locale: Locale): string {
if (locale === 'en') return iso;
const [y, m, d] = iso.split('-');
return d && m && y ? `${d}.${m}.${y}` : iso;
}

View File

@ -74,6 +74,7 @@ describe('documents', () => {
phone: '+49 69 000000',
website: 'www.muster.de',
logoDataUrl: '',
countries: [],
};
const md = riskReport(system, CONTENT_PACK, TODAY, profile);
expect(md).toContain('| **Unternehmen** | Muster GmbH |');

View File

@ -1,46 +1,20 @@
import type { Locale } from '../i18n/types';
import { docStrings, formatDocDate } from './docStrings';
import type { AISystem, CompanyProfile, ContentPack, Obligation } from './types';
const RISK_LABELS: Record<string, string> = {
'out-of-scope': 'Nicht anwendbar (keine KI i. S. d. Art. 3 Nr. 1)',
prohibited: 'VERBOTEN (Art. 5)',
high: 'Hochrisiko',
limited: 'Begrenztes Risiko (Transparenzpflichten)',
minimal: 'Minimales Risiko',
};
const ROLE_LABELS: Record<string, string> = {
provider: 'Anbieter (Provider)',
deployer: 'Betreiber (Deployer)',
importer: 'Einführer (Importer)',
distributor: 'Händler (Distributor)',
};
const STATUS_LABELS: Record<string, string> = {
open: 'Offen',
'in-progress': 'In Umsetzung',
done: 'Erledigt',
'not-applicable': 'Nicht anwendbar',
};
const DISCLAIMER =
'> **Hinweis:** Dieses Dokument wurde mit dem AI-Act-Kompass erstellt und ist eine strukturierte Arbeitshilfe — **keine Rechtsberatung**. Die rechtliche Bewertung im Einzelfall obliegt dem Unternehmen bzw. seiner Rechtsberatung.';
/** Formatiert ein ISO-Datum als deutsches Datum (TT.MM.JJJJ). */
function formatDate(iso: string): string {
const [y, m, d] = iso.split('-');
return d && m && y ? `${d}.${m}.${y}` : iso;
}
/** Kontaktzeile aus dem Firmenprofil (leer, wenn kein Profil gepflegt). */
function companyRows(profile: CompanyProfile | undefined): readonly string[] {
/** Kontaktzeilen aus dem Firmenprofil (leer, wenn kein Profil gepflegt). */
function companyRows(profile: CompanyProfile | undefined, locale: Locale): readonly string[] {
const s = docStrings(locale);
if (!profile || profile.name.trim() === '') return [];
const contact = [profile.street, profile.zipCity, profile.email, profile.phone, profile.website]
.filter((p) => p.trim() !== '')
.join(' · ');
return [
`| **Unternehmen** | ${profile.name} |`,
...(contact ? [`| **Kontakt** | ${contact} |`] : []),
...(profile.contactPerson.trim() ? [`| **Ansprechpartner** | ${profile.contactPerson} |`] : []),
`| **${s.meta.company}** | ${profile.name} |`,
...(contact ? [`| **${s.meta.contact}** | ${contact} |`] : []),
...(profile.contactPerson.trim()
? [`| **${s.meta.contactPerson}** | ${profile.contactPerson} |`]
: []),
];
}
@ -49,24 +23,26 @@ function header(
system: AISystem,
pack: ContentPack,
today: string,
locale: Locale,
profile?: CompanyProfile,
): string {
const s = docStrings(locale);
return [
`# ${title}`,
'',
`| | |`,
`|---|---|`,
...companyRows(profile),
`| **KI-System** | ${system.name} |`,
`| **Zweckbestimmung** | ${system.purpose || '—'} |`,
`| **Hersteller/Anbieter** | ${system.vendor || '—'} |`,
`| **Verantwortlich (intern)** | ${system.owner || '—'} |`,
`| **Rolle des Unternehmens** | ${ROLE_LABELS[system.answers.role] ?? system.answers.role} |`,
`| **Risikoklasse** | ${RISK_LABELS[system.classification.riskClass] ?? system.classification.riskClass} |`,
`| **Erstellt am** | ${formatDate(today)} |`,
`| **Content-Pack** | v${pack.version} (${pack.legalBasis}) |`,
...companyRows(profile, locale),
`| **${s.meta.system}** | ${system.name} |`,
`| **${s.meta.purpose}** | ${system.purpose || '—'} |`,
`| **${s.meta.vendor}** | ${system.vendor || '—'} |`,
`| **${s.meta.owner}** | ${system.owner || '—'} |`,
`| **${s.meta.role}** | ${s.role[system.answers.role]} |`,
`| **${s.meta.riskClass}** | ${s.risk[system.classification.riskClass]} |`,
`| **${s.meta.createdAt}** | ${formatDocDate(today, locale)} |`,
`| **${s.meta.contentPack}** | v${pack.version} (${pack.legalBasis}) |`,
'',
DISCLAIMER,
s.disclaimer,
'',
].join('\n');
}
@ -78,34 +54,32 @@ function header(
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
* @param profile - Firmenprofil für den Berichtskopf (optional)
* @param locale - Dokumentsprache (Default: Deutsch)
*/
export function riskReport(
system: AISystem,
pack: ContentPack,
today: string,
profile?: CompanyProfile,
locale: Locale = 'de',
): string {
const lines = [header('Risikoklassifizierung nach EU AI Act', system, pack, today, profile)];
const s = docStrings(locale);
const lines = [header(s.report.title, system, pack, today, locale, profile)];
lines.push('## Ergebnis', '');
lines.push(`**${RISK_LABELS[system.classification.riskClass] ?? system.classification.riskClass}**`, '');
lines.push(`## ${s.report.result}`, '');
lines.push(`**${s.risk[system.classification.riskClass]}**`, '');
lines.push('## Begründung', '');
lines.push(`## ${s.report.reasons}`, '');
for (const reason of system.classification.reasons) {
lines.push(`- ${reason}`);
}
lines.push('');
if (system.classification.gpaiObligations) {
lines.push('## GPAI', '', 'Für dieses System sind zusätzlich GPAI-Modellpflichten (Art. 53 ff.) relevant.', '');
lines.push(`## ${s.report.gpai}`, '', s.report.gpaiText, '');
}
if (system.classification.art64DocumentationDuty) {
lines.push(
'## Dokumentationspflicht Art. 6 Abs. 4',
'',
'Die Inanspruchnahme der Ausnahme nach Art. 6 Abs. 3 wurde dokumentiert. Diese Einschätzung ist auf Verlangen der Marktüberwachung vorzulegen.',
'',
);
lines.push(`## ${s.report.art64}`, '', s.report.art64Text, '');
}
return lines.join('\n');
@ -119,6 +93,7 @@ export function riskReport(
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
* @param profile - Firmenprofil für den Berichtskopf (optional)
* @param locale - Dokumentsprache (Default: Deutsch)
*/
export function actionPlan(
system: AISystem,
@ -126,22 +101,26 @@ export function actionPlan(
pack: ContentPack,
today: string,
profile?: CompanyProfile,
locale: Locale = 'de',
): string {
const lines = [header('Maßnahmenplan EU AI Act', system, pack, today, profile)];
const s = docStrings(locale);
const lines = [header(s.plan.title, system, pack, today, locale, profile)];
if (obligations.length === 0) {
lines.push('Für dieses System bestehen keine umsetzbaren Pflichten (verboten oder außerhalb des Anwendungsbereichs).');
lines.push(s.plan.none);
return lines.join('\n');
}
lines.push('## Pflichten', '');
lines.push('| Pflicht | Rechtsgrundlage | Frist | Status |');
lines.push(`## ${s.plan.obligations}`, '');
lines.push(`| ${s.plan.colObligation} | ${s.plan.colLegal} | ${s.plan.colDeadline} | ${s.plan.colStatus} |`);
lines.push('|---|---|---|---|');
for (const ob of obligations) {
const status = system.obligationStatus[ob.id] ?? 'open';
lines.push(`| ${ob.title} | ${ob.articles} | ${formatDate(ob.deadline)} | ${STATUS_LABELS[status]} |`);
lines.push(
`| ${ob.title} | ${ob.articles} | ${formatDocDate(ob.deadline, locale)} | ${s.status[status]} |`,
);
}
lines.push('', '## Details', '');
lines.push('', `## ${s.plan.details}`, '');
for (const ob of obligations) {
lines.push(`### ${ob.title} (${ob.articles})`, '', ob.description, '');
}
@ -149,6 +128,57 @@ export function actionPlan(
return lines.join('\n');
}
const ANNEX_IV_SECTIONS: Readonly<Record<Locale, readonly (readonly [string, readonly string[]])[]>> = {
de: [
['1. Allgemeine Beschreibung des KI-Systems', [
'Zweckbestimmung', 'Anbieter', 'Versionen und Datum des Inverkehrbringens',
'Hardware-Anforderungen / Bereitstellungsform', 'Interaktion mit externer Hard-/Software',
]],
['2. Detaillierte Beschreibung der Elemente und des Entwicklungsprozesses', [
'Entwicklungsmethoden und verwendete Werkzeuge', 'Systemarchitektur und Rechenressourcen',
'Datenanforderungen (Trainings-, Validierungs-, Testdaten, Herkunft, Aufbereitung)',
'Maßnahmen der menschlichen Aufsicht (Art. 14)',
'Vorab festgelegte Änderungen und Kontinuität der Konformität',
]],
['3. Überwachung, Funktionsweise und Kontrolle', [
'Fähigkeiten und Leistungsgrenzen (Genauigkeitsmetriken)',
'Vorhersehbare unbeabsichtigte Ergebnisse und Risikoquellen', 'Eingabedaten-Spezifikation',
]],
['4. Risikomanagementsystem (Art. 9)', ['Beschreibung des Risikomanagementprozesses']],
['5. Änderungen über den Lebenszyklus', ['Änderungshistorie und Update-Prozess']],
['6. Angewandte harmonisierte Normen und Spezifikationen', [
'Liste der Normen (z. B. ISO/IEC 42001, ISO/IEC 23894)',
]],
['7. EU-Konformitätserklärung', ['Kopie der Konformitätserklärung (Art. 47)']],
['8. Bewertung nach dem Inverkehrbringen (Art. 72)', [
'Plan zur Beobachtung nach dem Inverkehrbringen',
]],
],
en: [
['1. General description of the AI system', [
'Intended purpose', 'Provider', 'Versions and date of placing on the market',
'Hardware requirements / form of provision', 'Interaction with external hardware/software',
]],
['2. Detailed description of elements and development process', [
'Development methods and tools used', 'System architecture and computational resources',
'Data requirements (training, validation, test data; provenance, preparation)',
'Human oversight measures (Art. 14)',
'Pre-determined changes and continuity of conformity',
]],
['3. Monitoring, functioning and control', [
'Capabilities and performance limitations (accuracy metrics)',
'Foreseeable unintended outcomes and sources of risk', 'Input data specification',
]],
['4. Risk management system (Art. 9)', ['Description of the risk management process']],
['5. Changes over the lifecycle', ['Change history and update process']],
['6. Applied harmonised standards and specifications', [
'List of standards (e.g. ISO/IEC 42001, ISO/IEC 23894)',
]],
['7. EU declaration of conformity', ['Copy of the declaration of conformity (Art. 47)']],
['8. Post-market evaluation (Art. 72)', ['Post-market monitoring plan']],
],
};
/**
* Erzeugt das Grundgerüst der technischen Dokumentation nach Annex IV.
*
@ -159,61 +189,35 @@ export function actionPlan(
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
* @param profile - Firmenprofil für den Berichtskopf (optional)
* @param locale - Dokumentsprache (Default: Deutsch)
*/
export function annexIVSkeleton(
system: AISystem,
pack: ContentPack,
today: string,
profile?: CompanyProfile,
locale: Locale = 'de',
): string {
const todo = '_[AUSFÜLLEN]_';
const lines = [
header('Technische Dokumentation (Annex IV) — Grundgerüst', system, pack, today, profile),
];
const s = docStrings(locale);
const title =
locale === 'en'
? 'Technical documentation (Annex IV) — skeleton'
: 'Technische Dokumentation (Annex IV) — Grundgerüst';
const lines = [header(title, system, pack, today, locale, profile)];
lines.push(
'## 1. Allgemeine Beschreibung des KI-Systems',
'',
`- **Zweckbestimmung:** ${system.purpose || todo}`,
`- **Anbieter:** ${system.vendor || todo}`,
`- **Versionen und Datum des Inverkehrbringens:** ${todo}`,
`- **Hardware-Anforderungen / Bereitstellungsform:** ${todo}`,
`- **Interaktion mit externer Hard-/Software:** ${todo}`,
'',
'## 2. Detaillierte Beschreibung der Elemente und des Entwicklungsprozesses',
'',
`- **Entwicklungsmethoden und verwendete Werkzeuge:** ${todo}`,
`- **Systemarchitektur und Rechenressourcen:** ${todo}`,
`- **Datenanforderungen (Trainings-, Validierungs-, Testdaten, Herkunft, Aufbereitung):** ${todo}`,
`- **Maßnahmen der menschlichen Aufsicht (Art. 14):** ${todo}`,
`- **Vorab festgelegte Änderungen und Kontinuität der Konformität:** ${todo}`,
'',
'## 3. Überwachung, Funktionsweise und Kontrolle',
'',
`- **Fähigkeiten und Leistungsgrenzen (Genauigkeitsmetriken):** ${todo}`,
`- **Vorhersehbare unbeabsichtigte Ergebnisse und Risikoquellen:** ${todo}`,
`- **Eingabedaten-Spezifikation:** ${todo}`,
'',
'## 4. Risikomanagementsystem (Art. 9)',
'',
`- **Beschreibung des Risikomanagementprozesses:** ${todo}`,
'',
'## 5. Änderungen über den Lebenszyklus',
'',
`- **Änderungshistorie und Update-Prozess:** ${todo}`,
'',
'## 6. Angewandte harmonisierte Normen und Spezifikationen',
'',
`- **Liste der Normen (z. B. ISO/IEC 42001, ISO/IEC 23894):** ${todo}`,
'',
'## 7. EU-Konformitätserklärung',
'',
`- **Kopie der Konformitätserklärung (Art. 47):** ${todo}`,
'',
'## 8. Bewertung nach dem Inverkehrbringen (Art. 72)',
'',
`- **Plan zur Beobachtung nach dem Inverkehrbringen:** ${todo}`,
);
const prefilled: Readonly<Record<string, string>> = {
[ANNEX_IV_SECTIONS[locale][0]?.[1][0] ?? '']: system.purpose,
[ANNEX_IV_SECTIONS[locale][0]?.[1][1] ?? '']: system.vendor,
};
for (const [section, items] of ANNEX_IV_SECTIONS[locale]) {
lines.push(`## ${section}`, '');
for (const item of items) {
const value = prefilled[item];
lines.push(`- **${item}:** ${value && value.trim() !== '' ? value : s.todo}`);
}
lines.push('');
}
return lines.join('\n');
}
@ -225,31 +229,34 @@ export function annexIVSkeleton(
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
* @param profile - Firmenprofil für den Berichtskopf (optional)
* @param locale - Dokumentsprache (Default: Deutsch)
*/
export function inventoryReport(
systems: readonly AISystem[],
pack: ContentPack,
today: string,
profile?: CompanyProfile,
locale: Locale = 'de',
): string {
const s = docStrings(locale);
const company = profile && profile.name.trim() !== '' ? `${profile.name} · ` : '';
const lines = [
'# KI-Inventar (AI Asset Register)',
`# ${s.inventory.title}`,
'',
`${company}Stand: ${formatDate(today)} · Content-Pack v${pack.version}`,
`${company}${s.inventory.asOf}: ${formatDocDate(today, locale)} · Content-Pack v${pack.version}`,
'',
DISCLAIMER,
s.disclaimer,
'',
`Registrierte KI-Systeme: **${systems.length}**`,
`${s.inventory.count}: **${systems.length}**`,
'',
'| System | Zweck | Rolle | Risikoklasse | Verantwortlich |',
`| ${s.inventory.colSystem} | ${s.inventory.colPurpose} | ${s.inventory.colRole} | ${s.inventory.colRisk} | ${s.inventory.colOwner} |`,
'|---|---|---|---|---|',
];
for (const s of systems) {
for (const sys of systems) {
lines.push(
`| ${s.name} | ${s.purpose || '—'} | ${ROLE_LABELS[s.answers.role] ?? s.answers.role} | ${
RISK_LABELS[s.classification.riskClass] ?? s.classification.riskClass
} | ${s.owner || '—'} |`,
`| ${sys.name} | ${sys.purpose || '—'} | ${s.role[sys.answers.role]} | ${
s.risk[sys.classification.riskClass]
} | ${sys.owner || '—'} |`,
);
}
return lines.join('\n');

View File

@ -0,0 +1,189 @@
import type { Locale } from '../i18n/types';
import { docStrings, formatDocDate } from './docStrings';
import type { AISystem, CompanyProfile, ContentPack } from './types';
/** Anforderung im Lastenheft. */
interface Requirement {
readonly id: string;
readonly must: boolean;
readonly de: string;
readonly en: string;
/** Nur bei Hochrisiko aufnehmen. */
readonly highOnly?: boolean;
}
const REQUIREMENTS: readonly Requirement[] = [
{ id: 'REQ-01', must: true, highOnly: true,
de: 'CE-Kennzeichnung und EU-Konformitätserklärung (Art. 47, 48) liegen vor und werden mitgeliefert.',
en: 'CE marking and EU declaration of conformity (Art. 47, 48) exist and are supplied.' },
{ id: 'REQ-02', must: true, highOnly: true,
de: 'Registrierung des Systems in der EU-Datenbank (Art. 49) ist nachgewiesen.',
en: 'Registration of the system in the EU database (Art. 49) is evidenced.' },
{ id: 'REQ-03', must: true,
de: 'Vollständige Betriebsanleitung nach Art. 13 (Zweckbestimmung, Genauigkeit, Grenzen, Aufsicht, Wartung) in der Vertragssprache.',
en: 'Complete instructions for use under Art. 13 (purpose, accuracy, limitations, oversight, maintenance) in the contract language.' },
{ id: 'REQ-04', must: true, highOnly: true,
de: 'Logging-Fähigkeit nach Art. 12: automatische Ereignisprotokolle sind für den Betreiber zugänglich und mind. 6 Monate speicherbar.',
en: 'Logging capability under Art. 12: automatic event logs are accessible to the deployer and storable for at least 6 months.' },
{ id: 'REQ-05', must: true, highOnly: true,
de: 'Schnittstellen für menschliche Aufsicht nach Art. 14 (Eingriff, Abbruch, Übersteuerung) sind vorhanden und dokumentiert.',
en: 'Human oversight interfaces under Art. 14 (intervention, stop, override) exist and are documented.' },
{ id: 'REQ-06', must: true, highOnly: true,
de: 'Nachweis von Genauigkeit, Robustheit und Cybersicherheit nach Art. 15 inkl. Schutz gegen Prompt Injection, Data Poisoning und Adversarial Attacks.',
en: 'Evidence of accuracy, robustness and cybersecurity under Art. 15 incl. protection against prompt injection, data poisoning and adversarial attacks.' },
{ id: 'REQ-07', must: true,
de: 'Transparenzfunktionen nach Art. 50 (KI-Offenlegung, maschinenlesbare Kennzeichnung synthetischer Inhalte) sind implementiert, soweit einschlägig.',
en: 'Transparency features under Art. 50 (AI disclosure, machine-readable marking of synthetic content) are implemented where applicable.' },
{ id: 'REQ-08', must: true,
de: 'Unterstützung bei Vorfallmeldungen: Anbieter meldet schwerwiegende Vorfälle (Art. 73) und informiert den Auftraggeber unverzüglich.',
en: 'Incident reporting support: provider reports serious incidents (Art. 73) and informs the customer without undue delay.' },
{ id: 'REQ-09', must: true,
de: 'DSGVO-Konformität: Auftragsverarbeitungsvertrag, Angaben zu Datenflüssen, Speicherorten (EU), Unterauftragnehmern und Löschkonzept.',
en: 'GDPR conformity: data processing agreement, information on data flows, storage locations (EU), sub-processors and deletion concept.' },
{ id: 'REQ-10', must: false,
de: 'Update- und Support-Zusagen: Sicherheitsupdates, Re-Zertifizierung bei wesentlichen Änderungen, Reaktionszeiten.',
en: 'Update and support commitments: security updates, re-certification upon substantial modification, response times.' },
{ id: 'REQ-11', must: false,
de: 'Exit-Strategie: Datenexport in offenem Format, Übergabe von Konfiguration und Protokollen bei Vertragsende.',
en: 'Exit strategy: data export in an open format, handover of configuration and logs at contract end.' },
{ id: 'REQ-12', must: false, highOnly: true,
de: 'Einsicht in relevante Teile der technischen Dokumentation (Annex IV) unter Vertraulichkeit, soweit für Betreiberpflichten erforderlich.',
en: 'Access to relevant parts of the technical documentation (Annex IV) under confidentiality, insofar as required for deployer duties.' },
];
const PROC = {
de: {
title: 'Lastenheft KI-Beschaffung — AI-Act-Anforderungen an den Anbieter',
intro:
'Dieses Lastenheft definiert die regulatorischen Mindestanforderungen (EU AI Act, DSGVO) an den Anbieter des unten bezeichneten KI-Systems. Es ist Grundlage für Angebot, Pflichtenheft und Vertrag.',
colId: 'Nr.', colReq: 'Anforderung', colPrio: 'Priorität', colFulfilled: 'Erfüllt (Anbieter)',
must: 'MUSS', should: 'SOLL', fulfilled: 'JA / NEIN / TEILWEISE',
note: 'Hinweis: Bei Hochrisiko-Systemen gelten alle MUSS-Anforderungen zwingend; Nichterfüllung schließt die Beauftragung aus.',
},
en: {
title: 'AI procurement requirements specification — AI Act requirements for the provider',
intro:
'This requirements specification defines the minimum regulatory requirements (EU AI Act, GDPR) for the provider of the AI system identified below. It forms the basis for offer, implementation specification and contract.',
colId: 'No.', colReq: 'Requirement', colPrio: 'Priority', colFulfilled: 'Fulfilled (provider)',
must: 'MUST', should: 'SHOULD', fulfilled: 'YES / NO / PARTIALLY',
note: 'Note: For high-risk systems all MUST requirements are mandatory; non-fulfilment excludes the award.',
},
} as const;
/**
* Erzeugt das Lastenheft für die KI-Beschaffung (Betreiber-Perspektive):
* AI-Act-Anforderungen, die der Anbieter erfüllen und im Pflichtenheft
* beantworten muss.
*
* @param system - Bewertetes KI-System
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
* @param profile - Firmenprofil für den Berichtskopf (optional)
* @param locale - Dokumentsprache (Default: Deutsch)
*/
export function procurementSpec(
system: AISystem,
pack: ContentPack,
today: string,
profile?: CompanyProfile,
locale: Locale = 'de',
): string {
const s = docStrings(locale);
const p = PROC[locale];
const high = system.classification.riskClass === 'high';
const requirements = REQUIREMENTS.filter((r) => high || !r.highOnly);
const lines = [
`# ${p.title}`,
'',
`| | |`,
`|---|---|`,
...(profile && profile.name.trim() !== '' ? [`| **${s.meta.company}** | ${profile.name} |`] : []),
`| **${s.meta.system}** | ${system.name} |`,
`| **${s.meta.purpose}** | ${system.purpose || '—'} |`,
`| **${s.meta.riskClass}** | ${s.risk[system.classification.riskClass]} |`,
`| **${s.meta.createdAt}** | ${formatDocDate(today, locale)} |`,
`| **${s.meta.contentPack}** | v${pack.version} |`,
'',
s.disclaimer,
'',
p.intro,
'',
`| ${p.colId} | ${p.colReq} | ${p.colPrio} | ${p.colFulfilled} |`,
'|---|---|---|---|',
];
for (const r of requirements) {
lines.push(`| ${r.id} | ${r[locale]} | ${r.must ? p.must : p.should} | ${p.fulfilled} |`);
}
lines.push('', p.note);
return lines.join('\n');
}
const DECL = {
de: {
title: 'EU-Konformitätserklärung (Entwurf, Art. 47 / Annex V)',
intro:
'Entwurf auf Basis von Annex V der Verordnung (EU) 2024/1689. Vor Verwendung rechtlich prüfen und durch die Geschäftsleitung unterzeichnen.',
fields: [
['1. KI-System (Name, Typ, Version)', ''],
['2. Anbieter (Name und Anschrift)', ''],
['3. Verantwortung', 'Die alleinige Verantwortung für die Ausstellung dieser Konformitätserklärung trägt der Anbieter.'],
['4. Gegenstand der Erklärung', 'Das oben bezeichnete KI-System steht im Einklang mit der Verordnung (EU) 2024/1689 und ggf. weiteren einschlägigen Harmonisierungsvorschriften der Union.'],
['5. Angewandte harmonisierte Normen / gemeinsame Spezifikationen', '_[AUSFÜLLEN]_'],
['6. Notifizierte Stelle (falls einschlägig)', 'Name, Kennnummer, Bescheinigung: _[AUSFÜLLEN]_'],
['7. DSGVO-Erklärung (falls personenbezogene Daten verarbeitet werden)', 'Das System entspricht den Verordnungen (EU) 2016/679 und (EU) 2018/1725 bzw. der Richtlinie (EU) 2016/680.'],
['8. Ort, Datum der Ausstellung', '_[AUSFÜLLEN]_'],
['9. Unterzeichnet für und im Namen von', 'Name, Funktion, Unterschrift: _[AUSFÜLLEN]_'],
],
},
en: {
title: 'EU declaration of conformity (draft, Art. 47 / Annex V)',
intro:
'Draft based on Annex V of Regulation (EU) 2024/1689. Have it legally reviewed and signed by management before use.',
fields: [
['1. AI system (name, type, version)', ''],
['2. Provider (name and address)', ''],
['3. Responsibility', 'This declaration of conformity is issued under the sole responsibility of the provider.'],
['4. Object of the declaration', 'The AI system identified above is in conformity with Regulation (EU) 2024/1689 and, where applicable, other relevant Union harmonisation legislation.'],
['5. Applied harmonised standards / common specifications', '_[TO BE COMPLETED]_'],
['6. Notified body (where applicable)', 'Name, identification number, certificate: _[TO BE COMPLETED]_'],
['7. GDPR statement (where personal data is processed)', 'The system complies with Regulations (EU) 2016/679 and (EU) 2018/1725 or Directive (EU) 2016/680.'],
['8. Place and date of issue', '_[TO BE COMPLETED]_'],
['9. Signed for and on behalf of', 'Name, function, signature: _[TO BE COMPLETED]_'],
],
},
} as const;
/**
* Erzeugt den Entwurf der EU-Konformitätserklärung (Anbieter, Hochrisiko).
*
* @param system - Bewertetes KI-System
* @param pack - Regulatorischer Content-Pack
* @param today - Stichtag im ISO-Format
* @param profile - Firmenprofil (Anbieterangaben werden vorbefüllt)
* @param locale - Dokumentsprache (Default: Deutsch)
*/
export function conformityDeclaration(
system: AISystem,
_pack: ContentPack,
today: string,
profile?: CompanyProfile,
locale: Locale = 'de',
): string {
const s = docStrings(locale);
const d = DECL[locale];
const provider =
profile && profile.name.trim() !== ''
? [profile.name, profile.street, profile.zipCity].filter((x) => x.trim() !== '').join(', ')
: s.todo;
const lines = [`# ${d.title}`, '', s.disclaimer, '', d.intro, ''];
for (const [label, preset] of d.fields) {
let value: string = preset;
if (label.startsWith('1.')) value = `${system.name}${system.purpose ? `${system.purpose}` : ''}`;
if (label.startsWith('2.')) value = provider;
if (label.startsWith('8.')) value = `${profile?.zipCity ?? ''} ${formatDocDate(today, locale)}`.trim();
lines.push(`## ${label}`, '', value || s.todo, '');
}
return lines.join('\n');
}

View File

@ -0,0 +1,158 @@
import { describe, expect, it } from 'vitest';
import { CONTENT_PACK, getContentPack } from '../content/pack';
import { buildChecklist } from './checklist';
import { classify } from './classify';
import { conformityDeclaration, procurementSpec } from './extraDocs';
import { deriveObligations } from './obligations';
import type { AISystem, AssessmentAnswers, CompanyProfile } from './types';
const base: AssessmentAnswers = {
isAISystem: true,
role: 'deployer',
isGPAIModel: false,
prohibitedPractices: [],
annexIIICategories: [],
art63Exception: false,
involvesProfiling: false,
isAnnexISafetyComponent: false,
transparencyTriggers: [],
};
const profile: CompanyProfile = {
name: 'Muster GmbH',
street: 'Musterstraße 1',
zipCity: '60311 Frankfurt',
contactPerson: '',
email: '',
phone: '',
website: '',
logoDataUrl: '',
countries: ['DE', 'IT'],
};
function makeSystem(answers: AssessmentAnswers, locale: 'de' | 'en' = 'de'): AISystem {
return {
id: 'sys-1',
name: 'HR-Screening',
purpose: 'Bewerbervorauswahl',
vendor: 'Beispiel GmbH',
owner: 'Jane Doe',
createdAt: '2026-07-01T00:00:00.000Z',
updatedAt: '2026-07-01T00:00:00.000Z',
answers,
classification: classify(answers, getContentPack(locale), locale),
obligationStatus: {},
};
}
describe('mehrsprachiger Content-Pack', () => {
it('liefert identische IDs und Strukturen in DE und EN', () => {
const de = getContentPack('de');
const en = getContentPack('en');
expect(en.obligations.map((o) => o.id)).toEqual(de.obligations.map((o) => o.id));
expect(en.national.map((n) => n.code)).toEqual(de.national.map((n) => n.code));
expect(en.prohibitedPractices).toHaveLength(de.prohibitedPractices.length);
});
it('übersetzt Inhalte tatsächlich (kein deutscher Text im EN-Pack)', () => {
const en = getContentPack('en');
const literacy = en.obligations.find((o) => o.id === 'ob-ai-literacy');
expect(literacy?.title).toBe('Ensure AI literacy of staff');
expect(en.national.find((n) => n.code === 'DE')?.country).toBe('Germany');
});
it('klassifiziert mit englischen Begründungen', () => {
const result = classify(
{ ...base, annexIIICategories: ['a3-employment'] },
getContentPack('en'),
'en',
);
expect(result.riskClass).toBe('high');
expect(result.reasons[0]).toContain('Annex III area');
});
});
describe('nationale Module', () => {
const answers: AssessmentAnswers = { ...base, annexIIICategories: ['a3-employment'] };
it('ergänzt deutsche und italienische Pflichten bei aktivierten Modulen', () => {
const classification = classify(answers, CONTENT_PACK);
const ids = deriveObligations(classification, answers, CONTENT_PACK, ['DE', 'IT']).map((o) => o.id);
expect(ids).toContain('de-betrvg');
expect(ids).toContain('it-l132-workers');
expect(ids).not.toContain('es-aesia');
});
it('liefert ohne Länderauswahl nur EU-Pflichten', () => {
const classification = classify(answers, CONTENT_PACK);
const ids = deriveObligations(classification, answers, CONTENT_PACK).map((o) => o.id);
expect(ids.some((id) => id.startsWith('de-') || id.startsWith('it-'))).toBe(false);
});
it('respektiert Rolle und Risikoklasse auch bei nationalen Pflichten', () => {
const providerAnswers: AssessmentAnswers = { ...answers, role: 'provider' };
const classification = classify(providerAnswers, CONTENT_PACK);
const ids = deriveObligations(classification, providerAnswers, CONTENT_PACK, ['DE']).map((o) => o.id);
expect(ids).not.toContain('de-betrvg'); // nur Betreiber
expect(ids).toContain('de-kimig-authority');
});
it('enthält neue EU-Pflichten (Art. 72/73) für Hochrisiko-Anbieter', () => {
const providerAnswers: AssessmentAnswers = { ...answers, role: 'provider' };
const classification = classify(providerAnswers, CONTENT_PACK);
const ids = deriveObligations(classification, providerAnswers, CONTENT_PACK).map((o) => o.id);
expect(ids).toContain('ob-postmarket');
expect(ids).toContain('ob-incident');
expect(ids).toContain('ob-corrective');
});
});
describe('buildChecklist', () => {
it('gruppiert nach fällig / anstehend / erledigt und zählt Fortschritt', () => {
const system = makeSystem({ ...base, annexIIICategories: ['a3-employment'] });
const withStatus: AISystem = {
...system,
obligationStatus: { 'ob-ai-literacy': 'done' },
};
const checklist = buildChecklist([withStatus], CONTENT_PACK, ['DE'], '2026-07-04');
expect(checklist.total).toBeGreaterThan(5);
expect(checklist.resolvedCount).toBe(1);
// KI-Kompetenz (2025) ist erledigt; BetrVG (2025) ist fällig
expect(checklist.dueNow.some((e) => e.obligation.id === 'de-betrvg')).toBe(true);
// Art.-26-Pflichten (2027) stehen an
expect(checklist.upcoming.some((e) => e.obligation.id === 'ob-deployer-oversight')).toBe(true);
expect(checklist.resolved[0]?.obligation.id).toBe('ob-ai-literacy');
});
it('ist leer ohne Systeme', () => {
const checklist = buildChecklist([], CONTENT_PACK, [], '2026-07-04');
expect(checklist.total).toBe(0);
});
});
describe('Zusatzdokumente', () => {
it('Lastenheft enthält MUSS-Anforderungen und Hochrisiko-Punkte', () => {
const system = makeSystem({ ...base, annexIIICategories: ['a3-employment'] });
const md = procurementSpec(system, CONTENT_PACK, '2026-07-04', profile);
expect(md).toContain('Lastenheft');
expect(md).toContain('REQ-01');
expect(md).toContain('MUSS');
expect(md).toContain('keine Rechtsberatung');
});
it('Lastenheft lässt Hochrisiko-Anforderungen bei limited weg', () => {
const system = makeSystem({ ...base, transparencyTriggers: ['tr-chatbot'] });
const md = procurementSpec(system, CONTENT_PACK, '2026-07-04', profile);
expect(md).not.toContain('REQ-01'); // CE-Kennzeichnung nur Hochrisiko
expect(md).toContain('REQ-03'); // Betriebsanleitung immer
});
it('Konformitätserklärung füllt Anbieter aus dem Profil vor (EN)', () => {
const system = makeSystem({ ...base, role: 'provider', annexIIICategories: ['a3-employment'] }, 'en');
const md = conformityDeclaration(system, getContentPack('en'), '2026-07-04', profile, 'en');
expect(md).toContain('EU declaration of conformity');
expect(md).toContain('Muster GmbH, Musterstraße 1, 60311 Frankfurt');
expect(md).toContain('not legal advice');
});
});

View File

@ -5,35 +5,43 @@ import type { AssessmentAnswers, Classification, ContentPack, Obligation } from
*
* Auswahlkriterien je Pflicht: Risikoklasse und Rolle müssen passen;
* GPAI- und Transparenzpflichten werden nur bei entsprechendem Flag
* aufgenommen.
* aufgenommen. Zusätzlich werden die Pflichten der aktivierten
* nationalen Module (Länderauswahl) einbezogen.
*
* @param classification - Ergebnis von {@link classify}
* @param answers - Wizard-Antworten (Rolle, GPAI-Flag)
* @param pack - Regulatorischer Content-Pack
* @param countries - Aktivierte Länder-Module (ISO-Codes, z. B. ["DE","IT"])
* @returns Deduplizierte, nach Frist sortierte Pflichtenliste
*/
export function deriveObligations(
classification: Classification,
answers: AssessmentAnswers,
pack: ContentPack,
countries: readonly string[] = [],
): readonly Obligation[] {
if (classification.riskClass === 'out-of-scope' || classification.riskClass === 'prohibited') {
return [];
}
const applicable = pack.obligations.filter((ob) => {
const matches = (ob: Obligation): boolean => {
if (ob.id === 'ob-gpai') {
return classification.gpaiObligations && answers.role === 'provider';
}
if (ob.id === 'ob-transparency-art50') {
return classification.transparencyDuties && ob.roles.includes(answers.role);
}
return (
ob.riskClasses.includes(classification.riskClass) && ob.roles.includes(answers.role)
);
});
return ob.riskClasses.includes(classification.riskClass) && ob.roles.includes(answers.role);
};
return [...applicable].sort((a, b) => a.deadline.localeCompare(b.deadline));
const euObligations = pack.obligations.filter(matches);
const nationalObligations = pack.national
.filter((n) => countries.includes(n.code))
.flatMap((n) => n.items.filter(matches));
return [...euObligations, ...nationalObligations].sort((a, b) =>
a.deadline.localeCompare(b.deadline),
);
}
/**

View File

@ -92,6 +92,8 @@ export interface CompanyProfile {
readonly website: string;
/** Logo als Data-URL (bleibt lokal im Browser), leer = kein Logo. */
readonly logoDataUrl: string;
/** ISO-Ländercodes der aktivierten nationalen Anforderungsmodule (z. B. "DE"). */
readonly countries: readonly string[];
}
/** Eintrag im regulatorischen Content-Pack (Checklisten-Item mit Rechtsgrundlage). */
@ -110,6 +112,17 @@ export interface Deadline {
readonly description: string;
}
/** Nationales Anforderungsmodul eines EU-Mitgliedstaats. */
export interface NationalPack {
/** ISO-3166-Ländercode, z. B. "DE". */
readonly code: string;
readonly country: string;
/** Zuständige Aufsichts-/Marktüberwachungsbehörde(n). */
readonly authority: string;
/** Zusätzliche nationale Pflichten (Rechtsgrundlage im Feld articles). */
readonly items: readonly Obligation[];
}
/** Versionierter regulatorischer Content-Pack. */
export interface ContentPack {
readonly version: string;
@ -121,4 +134,6 @@ export interface ContentPack {
readonly aiDefinitionCriteria: readonly ContentItem[];
readonly obligations: readonly Obligation[];
readonly deadlines: readonly Deadline[];
/** Nationale Zusatzanforderungen der EU-Mitgliedstaaten. */
readonly national: readonly NationalPack[];
}

View File

@ -0,0 +1,47 @@
import { createContext, useContext, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { getContentPack } from '../content/pack';
import type { ContentPack } from '../engine/types';
import { loadLocale, saveLocale } from './types';
import type { Locale } from './types';
import { uiStrings } from './ui';
import type { UIStrings } from './ui';
interface I18nValue {
readonly locale: Locale;
readonly t: UIStrings;
/** Content-Pack in der aktuellen Sprache. */
readonly pack: ContentPack;
readonly setLocale: (locale: Locale) => void;
}
const I18nContext = createContext<I18nValue | null>(null);
/** Stellt Sprache, UI-Wörterbuch und lokalisierten Content-Pack bereit. */
export function I18nProvider({ children }: { readonly children: ReactNode }): JSX.Element {
const [locale, setLocaleState] = useState<Locale>(() => loadLocale());
const value = useMemo<I18nValue>(
() => ({
locale,
t: uiStrings(locale),
pack: getContentPack(locale),
setLocale: (next: Locale) => {
saveLocale(next);
setLocaleState(next);
},
}),
[locale],
);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
/** Zugriff auf Sprache, Wörterbuch und Content-Pack. */
export function useI18n(): I18nValue {
const ctx = useContext(I18nContext);
if (!ctx) {
throw new Error('useI18n muss innerhalb von <I18nProvider> verwendet werden.');
}
return ctx;
}

View File

@ -0,0 +1,48 @@
/**
* Mehrsprachigkeits-Grundlagen.
*
* Die App ist für europäische Unternehmen gedacht alle Inhalte
* (UI, Regel-Engine, Content-Pack, Dokumente) sind lokalisierbar.
* Aktuell vollständig: Deutsch und Englisch; weitere EU-Sprachen
* werden durch Erweitern von {@link LText} und den Wörterbüchern ergänzt.
*/
/** Unterstützte Sprachen. */
export type Locale = 'de' | 'en';
/** Alle unterstützten Sprachen in Anzeigereihenfolge. */
export const LOCALES: readonly Locale[] = ['de', 'en'];
/** Lokalisierter Text — eine Fassung je unterstützter Sprache. */
export interface LText {
readonly de: string;
readonly en: string;
}
/** Kurzform zum Anlegen lokalisierter Texte. */
export function lt(de: string, en: string): LText {
return { de, en };
}
/** Löst einen lokalisierten Text in der gewünschten Sprache auf. */
export function resolveText(text: LText, locale: Locale): string {
return text[locale];
}
const LOCALE_KEY = 'ai-act-kompass/locale/v1';
/** Lädt die gespeicherte Sprache (Fallback: Browser-Sprache, sonst Deutsch). */
export function loadLocale(): Locale {
try {
const stored = localStorage.getItem(LOCALE_KEY);
if (stored === 'de' || stored === 'en') return stored;
return navigator.language.toLowerCase().startsWith('de') ? 'de' : 'en';
} catch {
return 'de';
}
}
/** Persistiert die gewählte Sprache. */
export function saveLocale(locale: Locale): void {
localStorage.setItem(LOCALE_KEY, locale);
}

View File

@ -0,0 +1,317 @@
import type { Locale } from './types';
/* eslint-disable max-lines */
const DE = {
brandTag: 'EU AI Act Compliance für KMU · local-first',
navDashboard: 'Dashboard & Inventar',
navTasks: 'Aufgaben & Fristen',
navNew: 'Neues System bewerten',
navSettings: 'Einstellungen & Backup',
footPrivacy: 'Keine Cloud. Keine Telemetrie.',
footLocal: 'Alle Daten bleiben auf diesem Gerät.',
footNoAdvice: 'Keine Rechtsberatung.',
dashTitle: 'Dashboard',
dashSub: 'EU-AI-Act-Compliance auf einen Blick. Alle Daten bleiben lokal auf diesem Gerät.',
statSystems: 'KI-Systeme im Inventar',
statHigh: 'Hochrisiko-Systeme',
statProhibited: 'Verbotene Praktiken',
statOpen: 'Offene Aufgaben',
deadlinesTitle: 'Geltungsfristen des AI Act',
inventoryTitle: 'KI-Inventar',
inventoryPrint: 'Inventar drucken / PDF',
newSystem: '+ System bewerten',
emptyInventory:
'Noch keine KI-Systeme erfasst. Starten Sie mit der Bewertung Ihres ersten Systems — z. B. Ihres Chatbots, Ihres Bewerber-Screenings oder eines eingekauften KI-Tools.',
colSystem: 'System',
colRole: 'Rolle',
colRisk: 'Risikoklasse',
colOwner: 'Verantwortlich',
colUpdated: 'Zuletzt bewertet',
disclaimerLine:
'Der AI-Act-Kompass ist eine strukturierte Arbeitshilfe auf Basis der Verordnung (EU) 2024/1689 — keine Rechtsberatung. Content-Pack v{version}.',
tasksTitle: 'Aufgaben & Fristen',
tasksSub: 'Alle Pflichten aller Systeme als abhakbare Liste — was erledigt ist, was jetzt fällig ist und was noch kommt.',
tasksProgress: '{done} von {total} Aufgaben erledigt',
tasksDueNow: 'Jetzt fällig',
tasksUpcoming: 'Kommt auf Sie zu',
tasksResolved: 'Erledigt',
tasksEmpty: 'Keine Aufgaben — bewerten Sie zuerst ein KI-System.',
tasksAllDone: 'Alles erledigt in dieser Gruppe.',
tasksDue: 'Frist',
wizardTitleNew: 'Neues KI-System bewerten',
wizardTitleRe: 'Neubewertung: {name}',
wizardSub: 'Beantworten Sie die Fragen — die Risikoklasse wird automatisch nach der Systematik des AI Act ermittelt.',
stepBasics: 'Grunddaten',
stepDefinition: 'KI-Definition',
stepProhibited: 'Verbote',
stepHighRisk: 'Hochrisiko',
stepTransparency: 'Transparenz & GPAI',
stepResult: 'Ergebnis',
fieldName: 'Name des Systems *',
fieldNamePh: 'z. B. Bewerber-Screening-Tool',
fieldPurpose: 'Zweckbestimmung',
fieldPurposePh: 'Wofür wird das System eingesetzt?',
fieldVendor: 'Hersteller / Anbieter',
fieldOwner: 'Verantwortliche Person (intern)',
fieldRole: 'Rolle Ihres Unternehmens',
roleHint: 'Anbieter = entwickelt/vertreibt das System unter eigenem Namen. Betreiber = setzt ein fremdes System ein (häufigster KMU-Fall).',
defTitle: 'Erfüllt das System die KI-Definition (Art. 3 Nr. 1)?',
defHint: 'Nur wenn alle Kriterien zutreffen, fällt das System unter den AI Act.',
defWarn: 'Nicht alle Kriterien erfüllt → das System gilt nicht als KI-System, der AI Act ist nicht anwendbar.',
prohTitle: 'Verbotene Praktiken (Art. 5)',
prohHint: 'Trifft einer dieser Punkte zu, ist der Einsatz seit 2. Februar 2025 verboten.',
hrTitle: 'Hochrisiko-Einstufung (Art. 6)',
annexILabel: 'Das System ist Sicherheitsbauteil eines regulierten Produkts (z. B. Medizinprodukt, Maschine, Aufzug) oder selbst ein solches Produkt.',
annexIIITitle: 'Einsatzbereiche nach Annex III',
exceptionTitle: 'Mögliche Ausnahme (Art. 6 Abs. 3)',
profilingLabel: 'Das System führt Profiling natürlicher Personen durch.',
profilingRef: 'Profiling schließt die Ausnahme aus — immer Hochrisiko.',
art63Label: 'Das System erfüllt nur eine eng begrenzte verfahrenstechnische oder vorbereitende Aufgabe bzw. verbessert lediglich das Ergebnis einer zuvor abgeschlossenen menschlichen Tätigkeit — ohne wesentliches Risiko für Gesundheit, Sicherheit oder Grundrechte.',
art63Ref: 'Art. 6 Abs. 3 — die Einschätzung muss dokumentiert werden (Art. 6 Abs. 4).',
trTitle: 'Transparenzpflichten (Art. 50)',
gpaiTitle: 'General-Purpose AI',
gpaiLabel: 'Wir stellen ein eigenes GPAI-Modell bereit (z. B. selbst trainiertes oder wesentlich verändertes Basismodell).',
gpaiRef: 'Art. 51 ff. — die bloße Nutzung von z. B. GPT/Claude über API zählt nicht.',
resultTitle: 'Ergebnis:',
resultGpai: 'Zusätzlich gelten GPAI-Modellpflichten (Art. 53 ff.).',
resultProhibited: 'Der Einsatz dieses Systems ist nach Art. 5 verboten (Bußgeld bis 35 Mio. € oder 7 % des weltweiten Jahresumsatzes). Einsatz einstellen und rechtlichen Rat einholen.',
btnCancel: 'Abbrechen',
btnBack: 'Zurück',
btnNext: 'Weiter',
btnSave: 'Speichern',
detailBack: '← Zurück zum Dashboard',
detailNoPurpose: 'Keine Zweckbestimmung erfasst',
detailReasons: 'Begründung der Einstufung',
detailReassess: 'Neu bewerten',
detailDelete: 'System löschen',
detailObligations: 'Pflichten',
detailNational: 'inkl. Länder-Module: {codes}',
detailDocs: 'Dokumente generieren',
docShow: 'ansehen',
docHide: 'ausblenden',
docPrint: 'Drucken / PDF',
docRisk: 'Risikobericht',
docPlan: 'Maßnahmenplan',
docAnnexIV: 'Annex-IV-Dokumentation',
docProcurement: 'Lastenheft KI-Beschaffung',
docConformity: 'EU-Konformitätserklärung',
confirmDeleteSystem: '„{name}" wirklich löschen?',
systemNotFound: 'System nicht gefunden.',
statusOpen: 'Offen',
statusInProgress: 'In Umsetzung',
statusDone: 'Erledigt',
statusNA: 'Nicht anwendbar',
settingsTitle: 'Einstellungen',
settingsSub: 'Datenhoheit: Alles liegt lokal in diesem Browser — kein Cloud-Sync, keine Telemetrie.',
langTitle: 'Sprache / Language',
langHint: 'Benutzeroberfläche, Inhalte und generierte Dokumente. Weitere EU-Sprachen folgen über den Content-Feed.',
profileTitle: 'Firmenprofil (Briefkopf für Berichte & PDF-Druck)',
profileHint: 'Diese Angaben erscheinen im Kopf und in der Fußzeile aller generierten und gedruckten Dokumente. Das Logo bleibt lokal gespeichert und verlässt das Gerät nicht.',
pfName: 'Firmenname', pfStreet: 'Straße & Hausnummer', pfZip: 'PLZ & Ort',
pfContact: 'Ansprechpartner', pfEmail: 'E-Mail', pfPhone: 'Telefon', pfWeb: 'Website',
logoNone: 'Kein Logo hinterlegt.',
logoUpload: 'Logo hochladen',
logoRemove: 'Logo entfernen',
logoTaken: 'Logo übernommen.',
logoTooBig: 'Logo zu groß — bitte eine Datei unter 300 KB verwenden.',
logoNotImage: 'Bitte eine Bilddatei wählen (PNG, JPG oder SVG).',
countriesTitle: 'Länder-Module (nationale Zusatzanforderungen)',
countriesHint: 'Aktivieren Sie die EU-Länder, in denen Ihr Unternehmen KI-Systeme anbietet oder betreibt. Die nationalen Pflichten (Aufsicht, Arbeitsrecht, Sondergesetze) erscheinen dann in Checklisten und Dokumenten. Bei grenzüberschreitender Zusammenarbeit: alle beteiligten Länder aktivieren.',
authorityLabel: 'Aufsicht',
packTitle: 'Regulatorischer Content-Pack',
packStand: 'Stand',
packHint: 'Der Content-Pack (Verbote, Annex-III-Bereiche, EU-Pflichten, Länder-Module, Fristen) ist als austauschbares Datenpaket modelliert. Updates erscheinen mit neuen App-Versionen bzw. künftig über einen signierten Update-Feed.',
backupTitle: 'Datensicherung',
backupExport: 'Backup exportieren (JSON)',
backupImport: 'Backup importieren',
importOk: 'Import abgeschlossen: {count} System(e){profile}.',
importProfileSuffix: ' inkl. Firmenprofil',
importFail: 'Import fehlgeschlagen: {error}',
deleteTitle: 'Alle Daten löschen',
deleteHint: 'Entfernt alle erfassten KI-Systeme und das Firmenprofil unwiderruflich aus diesem Browser.',
deleteBtn: 'Alles löschen',
deleteConfirm: 'Wirklich alle lokalen Daten unwiderruflich löschen?',
deleteDone: 'Alle Daten gelöscht.',
legalGateTitle: 'Wichtiger rechtlicher Hinweis',
legalGateBody1:
'Der AI-Act-Kompass ist eine strukturierte Arbeitshilfe, die die Umsetzung der Verordnung (EU) 2024/1689 (EU AI Act) und einschlägiger nationaler Vorschriften erleichtern soll.',
legalGateBody2:
'Diese Anwendung erbringt keine Rechtsdienstleistung im Sinne des Rechtsdienstleistungsgesetzes (RDG) und stellt keine Rechtsberatung, Rechtsauskunft oder rechtsverbindliche Belehrung dar. Alle Einstufungen, Checklisten und generierten Dokumente sind unverbindliche Ersteinschätzungen und Entwürfe ohne Gewähr für Richtigkeit, Vollständigkeit oder Aktualität.',
legalGateBody3:
'Die verbindliche Auslegung des EU AI Act obliegt allein den zuständigen Behörden und Gerichten. Die rechtliche Bewertung Ihres Einzelfalls ersetzt dieses Tool nicht — ziehen Sie hierfür qualifizierte Rechtsberatung (Rechtsanwältin/Rechtsanwalt) hinzu. Die Nutzung erfolgt auf eigenes Risiko; eine Haftung für Entscheidungen auf Grundlage dieses Tools ist ausgeschlossen, soweit gesetzlich zulässig.',
legalGateAccept: 'Verstanden — als Arbeitshilfe nutzen',
legalNoticeTitle: 'Rechtlicher Hinweis (keine Rechtsberatung)',
resultDisclaimer:
'Diese Einstufung ist eine unverbindliche, automatisierte Ersteinschätzung auf Basis Ihrer Angaben — keine Rechtsberatung. Lassen Sie das Ergebnis vor weitreichenden Entscheidungen rechtlich prüfen.',
};
export type UIStrings = typeof DE;
const EN: UIStrings = {
brandTag: 'EU AI Act compliance for SMEs · local-first',
navDashboard: 'Dashboard & inventory',
navTasks: 'Tasks & deadlines',
navNew: 'Assess new system',
navSettings: 'Settings & backup',
footPrivacy: 'No cloud. No telemetry.',
footLocal: 'All data stays on this device.',
footNoAdvice: 'Not legal advice.',
dashTitle: 'Dashboard',
dashSub: 'EU AI Act compliance at a glance. All data stays locally on this device.',
statSystems: 'AI systems in inventory',
statHigh: 'High-risk systems',
statProhibited: 'Prohibited practices',
statOpen: 'Open tasks',
deadlinesTitle: 'AI Act application deadlines',
inventoryTitle: 'AI inventory',
inventoryPrint: 'Print inventory / PDF',
newSystem: '+ Assess system',
emptyInventory:
'No AI systems recorded yet. Start by assessing your first system — e.g. your chatbot, your applicant screening or a purchased AI tool.',
colSystem: 'System',
colRole: 'Role',
colRisk: 'Risk class',
colOwner: 'Responsible',
colUpdated: 'Last assessed',
disclaimerLine:
'AI-Act-Kompass is a structured working aid based on Regulation (EU) 2024/1689 — not legal advice. Content pack v{version}.',
tasksTitle: 'Tasks & deadlines',
tasksSub: 'All obligations of all systems as a checkable list — what is done, what is due now and what is coming up.',
tasksProgress: '{done} of {total} tasks done',
tasksDueNow: 'Due now',
tasksUpcoming: 'Coming up',
tasksResolved: 'Done',
tasksEmpty: 'No tasks — assess an AI system first.',
tasksAllDone: 'Everything done in this group.',
tasksDue: 'Deadline',
wizardTitleNew: 'Assess a new AI system',
wizardTitleRe: 'Reassessment: {name}',
wizardSub: 'Answer the questions — the risk class is determined automatically following the AI Acts system.',
stepBasics: 'Basics',
stepDefinition: 'AI definition',
stepProhibited: 'Prohibitions',
stepHighRisk: 'High-risk',
stepTransparency: 'Transparency & GPAI',
stepResult: 'Result',
fieldName: 'System name *',
fieldNamePh: 'e.g. applicant screening tool',
fieldPurpose: 'Intended purpose',
fieldPurposePh: 'What is the system used for?',
fieldVendor: 'Manufacturer / provider',
fieldOwner: 'Responsible person (internal)',
fieldRole: 'Your companys role',
roleHint: 'Provider = develops/markets the system under its own name. Deployer = uses a third-party system (most common SME case).',
defTitle: 'Does the system meet the AI definition (Art. 3(1))?',
defHint: 'Only if all criteria apply does the system fall under the AI Act.',
defWarn: 'Not all criteria met → the system does not qualify as an AI system; the AI Act does not apply.',
prohTitle: 'Prohibited practices (Art. 5)',
prohHint: 'If any of these apply, use has been prohibited since 2 February 2025.',
hrTitle: 'High-risk classification (Art. 6)',
annexILabel: 'The system is a safety component of a regulated product (e.g. medical device, machine, lift) or such a product itself.',
annexIIITitle: 'Areas of use under Annex III',
exceptionTitle: 'Possible exception (Art. 6(3))',
profilingLabel: 'The system performs profiling of natural persons.',
profilingRef: 'Profiling excludes the exception — always high-risk.',
art63Label: 'The system only performs a narrow procedural or preparatory task, or merely improves the result of a previously completed human activity — without significant risk to health, safety or fundamental rights.',
art63Ref: 'Art. 6(3) — the assessment must be documented (Art. 6(4)).',
trTitle: 'Transparency obligations (Art. 50)',
gpaiTitle: 'General-purpose AI',
gpaiLabel: 'We provide our own GPAI model (e.g. self-trained or substantially modified base model).',
gpaiRef: 'Art. 51 et seq. — merely using e.g. GPT/Claude via API does not count.',
resultTitle: 'Result:',
resultGpai: 'GPAI model obligations (Art. 53 et seq.) apply additionally.',
resultProhibited: 'Use of this system is prohibited under Art. 5 (fines up to €35m or 7% of global annual turnover). Cease use and seek legal advice.',
btnCancel: 'Cancel',
btnBack: 'Back',
btnNext: 'Next',
btnSave: 'Save',
detailBack: '← Back to dashboard',
detailNoPurpose: 'No intended purpose recorded',
detailReasons: 'Reasoning for the classification',
detailReassess: 'Reassess',
detailDelete: 'Delete system',
detailObligations: 'Obligations',
detailNational: 'incl. country modules: {codes}',
detailDocs: 'Generate documents',
docShow: 'view',
docHide: 'hide',
docPrint: 'Print / PDF',
docRisk: 'Risk report',
docPlan: 'Action plan',
docAnnexIV: 'Annex IV documentation',
docProcurement: 'AI procurement specification',
docConformity: 'EU declaration of conformity',
confirmDeleteSystem: 'Really delete “{name}”?',
systemNotFound: 'System not found.',
statusOpen: 'Open',
statusInProgress: 'In progress',
statusDone: 'Done',
statusNA: 'Not applicable',
settingsTitle: 'Settings',
settingsSub: 'Data sovereignty: everything stays locally in this browser — no cloud sync, no telemetry.',
langTitle: 'Sprache / Language',
langHint: 'User interface, content and generated documents. More EU languages will follow via the content feed.',
profileTitle: 'Company profile (letterhead for reports & PDF printing)',
profileHint: 'These details appear in the header and footer of all generated and printed documents. The logo is stored locally and never leaves the device.',
pfName: 'Company name', pfStreet: 'Street & number', pfZip: 'Postcode & city',
pfContact: 'Contact person', pfEmail: 'Email', pfPhone: 'Phone', pfWeb: 'Website',
logoNone: 'No logo stored.',
logoUpload: 'Upload logo',
logoRemove: 'Remove logo',
logoTaken: 'Logo applied.',
logoTooBig: 'Logo too large — please use a file under 300 KB.',
logoNotImage: 'Please choose an image file (PNG, JPG or SVG).',
countriesTitle: 'Country modules (national additional requirements)',
countriesHint: 'Activate the EU countries in which your company offers or operates AI systems. National obligations (supervision, labour law, special laws) then appear in checklists and documents. For cross-border cooperation: activate all countries involved.',
authorityLabel: 'Supervision',
packTitle: 'Regulatory content pack',
packStand: 'As of',
packHint: 'The content pack (prohibitions, Annex III areas, EU obligations, country modules, deadlines) is modelled as a replaceable data package. Updates ship with new app versions and, in future, via a signed update feed.',
backupTitle: 'Backup',
backupExport: 'Export backup (JSON)',
backupImport: 'Import backup',
importOk: 'Import completed: {count} system(s){profile}.',
importProfileSuffix: ' incl. company profile',
importFail: 'Import failed: {error}',
deleteTitle: 'Delete all data',
deleteHint: 'Irreversibly removes all recorded AI systems and the company profile from this browser.',
deleteBtn: 'Delete everything',
deleteConfirm: 'Really delete all local data irreversibly?',
deleteDone: 'All data deleted.',
legalGateTitle: 'Important legal notice',
legalGateBody1:
'AI-Act-Kompass is a structured working aid intended to facilitate the implementation of Regulation (EU) 2024/1689 (EU AI Act) and relevant national provisions.',
legalGateBody2:
'This application does not provide legal services and does not constitute legal advice, legal information or legally binding instruction. All classifications, checklists and generated documents are non-binding preliminary assessments and drafts, without warranty as to accuracy, completeness or currency.',
legalGateBody3:
'The binding interpretation of the EU AI Act rests solely with the competent authorities and courts. This tool does not replace the legal assessment of your individual case — consult qualified legal counsel for that purpose. Use is at your own risk; liability for decisions based on this tool is excluded to the extent permitted by law.',
legalGateAccept: 'Understood — use as a working aid',
legalNoticeTitle: 'Legal notice (not legal advice)',
resultDisclaimer:
'This classification is a non-binding, automated preliminary assessment based on your input — not legal advice. Have the result legally reviewed before far-reaching decisions.',
};
const DICTS: Readonly<Record<Locale, UIStrings>> = { de: DE, en: EN };
/** Liefert das UI-Wörterbuch für die Sprache. */
export function uiStrings(locale: Locale): UIStrings {
return DICTS[locale];
}
/** Ersetzt {platzhalter} in einem UI-String. */
export function fmt(template: string, params: Readonly<Record<string, string | number>>): string {
return template.replace(/\{(\w+)\}/g, (_, key: string) => String(params[key] ?? ''));
}

View File

@ -1,5 +1,6 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nProvider } from './i18n/context';
import { App } from './ui/App';
import './styles.css';
@ -10,6 +11,8 @@ if (!container) {
createRoot(container).render(
<StrictMode>
<I18nProvider>
<App />
</I18nProvider>
</StrictMode>,
);

View File

@ -30,6 +30,7 @@ export function emptyProfile(): CompanyProfile {
phone: '',
website: '',
logoDataUrl: '',
countries: [],
};
}

View File

@ -289,3 +289,89 @@ input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); ou
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 16px; }
.empty { text-align: center; color: var(--muted); padding: 44px 0; font-size: 14.5px; font-style: italic; }
.panel-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }
.table-scroll { overflow-x: auto; }
/* ── Sprachumschalter ─────────────────────────────────────────────── */
.lang-switch { display: flex; gap: 0; margin-top: 18px; border: 1px solid var(--ink); align-self: flex-start; }
.lang-switch button {
border: none; border-right: 1px solid var(--ink); border-radius: 0;
padding: 6px 12px; background: var(--panel); color: var(--ink);
font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.06em; cursor: pointer;
}
.lang-switch button:last-child { border-right: none; }
.lang-switch button.active { background: var(--ink); color: var(--paper); }
/* ── Aufgabenliste (abhakbar) ─────────────────────────────────────── */
.task-list { list-style: none; padding: 0; margin: 0; }
.task {
display: flex; gap: 12px; align-items: flex-start;
padding: 11px 4px; border-bottom: 1px solid var(--hairline);
}
.task:last-child { border-bottom: none; }
.task.done .task-title { text-decoration: line-through; color: var(--muted); }
.task-check input { width: 19px; height: 19px; margin-top: 2px; accent-color: var(--accent); cursor: pointer; }
.task-body { flex: 1; min-width: 0; }
.task-title { font-size: 14.5px; font-weight: 600; }
.task-meta {
display: flex; gap: 12px; flex-wrap: wrap; margin-top: 3px;
font-family: var(--mono); font-size: 11px; color: var(--muted);
}
.task-system {
border: none; background: none; padding: 0; cursor: pointer;
font-family: var(--mono); font-size: 11px; color: var(--accent);
text-decoration: underline; text-underline-offset: 2px;
}
.progress-label { font-family: var(--mono); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
.progress-track { height: 12px; border: 1px solid var(--ink); background: var(--panel); }
.progress-fill { height: 100%; background: var(--ink); transition: width 0.3s ease; }
/* ── Rechtlicher Hinweis (Erststart) ──────────────────────────────── */
.gate-backdrop {
position: fixed; inset: 0; z-index: 100;
background: rgba(20, 18, 14, 0.6);
display: flex; align-items: center; justify-content: center; padding: 20px;
}
.gate {
background: var(--paper); border: 2px solid var(--ink);
max-width: 620px; max-height: 90vh; overflow-y: auto; padding: 26px 28px;
}
.gate h1 { border-bottom: 3px double var(--ink); padding-bottom: 10px; }
.gate p { font-size: 14.5px; line-height: 1.6; }
.gate button { margin-top: 10px; }
/* ── Responsiv: Tablet & Handy ────────────────────────────────────── */
@media (max-width: 900px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%; border-right: none; border-bottom: 2px solid var(--ink);
padding: 14px 16px; flex-direction: column;
}
.sidebar .brand { font-size: 17px; padding-bottom: 8px; }
.sidebar nav {
flex-direction: row; overflow-x: auto; margin-top: 10px; gap: 0;
-webkit-overflow-scrolling: touch;
}
.sidebar nav button {
white-space: nowrap; border-bottom: none; border-right: 1px solid var(--hairline);
padding: 9px 12px; font-size: 13.5px;
}
.sidebar button.active { border-left: none; padding-left: 12px; background: var(--ink); color: var(--paper); }
.sidebar .foot { display: none; }
.lang-switch { position: absolute; top: 14px; right: 16px; margin-top: 0; }
.sidebar { position: relative; }
.main { padding: 18px 14px; }
h1 { font-size: 21px; }
.panel { padding: 14px 14px; }
.grid { grid-template-columns: repeat(2, 1fr); gap: 12px 0; }
.grid .stat:nth-child(odd) { border-left: none; padding-left: 0; }
.grid .stat:nth-child(even) { padding-left: 14px; }
.stat .num { font-size: 26px; }
.wizard-steps { border: none; gap: 4px; }
.wizard-steps .step { border: 1px solid var(--hairline); }
.deadline-list li { flex-direction: column; gap: 2px; }
button.primary, button.ghost, button.danger { padding: 10px 14px; }
.task { padding: 12px 2px; }
.task-check input { width: 22px; height: 22px; }
}

View File

@ -1,19 +1,26 @@
import { useEffect, useState } from 'react';
import type { AISystem, CompanyProfile } from '../engine/types';
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 { ChecklistView } from './ChecklistView';
import { Dashboard } from './Dashboard';
import { DisclaimerGate } from './DisclaimerGate';
import { SettingsView } from './SettingsView';
import { SystemDetail } from './SystemDetail';
import { Wizard } from './Wizard';
type View =
| { readonly kind: 'dashboard' }
| { readonly kind: 'tasks' }
| { readonly kind: 'wizard'; readonly systemId: string | null }
| { readonly kind: 'detail'; readonly systemId: string }
| { readonly kind: 'settings' };
/** App-Shell: Navigation, State-Verwaltung und Persistenz. */
export function App(): JSX.Element {
const { t, locale, setLocale } = useI18n();
const [systems, setSystems] = useState<readonly AISystem[]>(() => loadSystems());
const [profile, setProfile] = useState<CompanyProfile>(() => loadProfile());
const [view, setView] = useState<View>({ kind: 'dashboard' });
@ -40,40 +47,73 @@ export function App(): JSX.Element {
setView({ kind: 'dashboard' });
};
const setObligationStatus = (
systemId: string,
obligationId: string,
status: ObligationStatus,
): void => {
const system = findSystem(systemId);
if (!system) return;
upsert({
...system,
updatedAt: new Date().toISOString(),
obligationStatus: { ...system.obligationStatus, [obligationId]: status },
});
};
return (
<div className="layout">
<DisclaimerGate />
<aside className="sidebar">
<div className="brand">
AI-Act-Kompass
<small>EU AI Act Compliance für KMU · local-first</small>
<small>{t.brandTag}</small>
</div>
<nav>
<button
className={view.kind === 'dashboard' ? 'active' : ''}
onClick={() => setView({ kind: 'dashboard' })}
>
Dashboard & Inventar
{t.navDashboard}
</button>
<button
className={view.kind === 'tasks' ? 'active' : ''}
onClick={() => setView({ kind: 'tasks' })}
>
{t.navTasks}
</button>
<button
className={view.kind === 'wizard' ? 'active' : ''}
onClick={() => setView({ kind: 'wizard', systemId: null })}
>
Neues System bewerten
{t.navNew}
</button>
<button
className={view.kind === 'settings' ? 'active' : ''}
onClick={() => setView({ kind: 'settings' })}
>
Einstellungen & Backup
{t.navSettings}
</button>
</nav>
<div className="lang-switch">
{LOCALES.map((l) => (
<button
key={l}
className={l === locale ? 'active' : ''}
onClick={() => setLocale(l)}
aria-label={`Sprache ${l}`}
>
{l.toUpperCase()}
</button>
))}
</div>
<div className="foot">
Keine Cloud. Keine Telemetrie.
{t.footPrivacy}
<br />
Alle Daten bleiben auf diesem Gerät.
{t.footLocal}
<br />
<br />
Keine Rechtsberatung.
{t.footNoAdvice}
</div>
</aside>
@ -87,6 +127,17 @@ export function App(): JSX.Element {
/>
)}
{view.kind === 'tasks' && (
<ChecklistView
systems={systems}
profile={profile}
onToggle={(systemId, obligationId, done) =>
setObligationStatus(systemId, obligationId, done ? 'done' : 'open')
}
onOpenSystem={(id) => setView({ kind: 'detail', systemId: id })}
/>
)}
{view.kind === 'wizard' && (
<Wizard
existing={view.systemId ? (findSystem(view.systemId) ?? null) : null}
@ -102,7 +153,7 @@ export function App(): JSX.Element {
(() => {
const system = findSystem(view.systemId);
if (!system) {
return <div className="empty">System nicht gefunden.</div>;
return <div className="empty">{t.systemNotFound}</div>;
}
return (
<SystemDetail
@ -111,7 +162,7 @@ export function App(): JSX.Element {
onUpdate={upsert}
onReassess={() => setView({ kind: 'wizard', systemId: system.id })}
onDelete={() => {
if (window.confirm(`${system.name}" wirklich löschen?`)) {
if (window.confirm(fmt(t.confirmDeleteSystem, { name: system.name }))) {
remove(system.id);
}
}}

View File

@ -0,0 +1,133 @@
import { useMemo } from 'react';
import { buildChecklist } from '../engine/checklist';
import type { ChecklistEntry } from '../engine/checklist';
import type { AISystem, CompanyProfile } from '../engine/types';
import { useI18n } from '../i18n/context';
import { fmt } from '../i18n/ui';
import { todayISO, uiDate } from './common';
interface ChecklistViewProps {
readonly systems: readonly AISystem[];
readonly profile: CompanyProfile;
readonly onToggle: (systemId: string, obligationId: string, done: boolean) => void;
readonly onOpenSystem: (systemId: string) => void;
}
function Group({
title,
entries,
emptyText,
onToggle,
onOpenSystem,
}: {
readonly title: string;
readonly entries: readonly ChecklistEntry[];
readonly emptyText: string;
readonly onToggle: (systemId: string, obligationId: string, done: boolean) => void;
readonly onOpenSystem: (systemId: string) => void;
}): JSX.Element {
const { locale, t } = useI18n();
return (
<div className="panel">
<h2 style={{ marginTop: 0 }}>
{title} ({entries.length})
</h2>
{entries.length === 0 ? (
<p className="hint">{emptyText}</p>
) : (
<ul className="task-list">
{entries.map((e) => (
<li key={`${e.systemId}:${e.obligation.id}`} className={e.resolved ? 'task done' : 'task'}>
<label className="task-check">
<input
type="checkbox"
checked={e.resolved}
onChange={(ev) => onToggle(e.systemId, e.obligation.id, ev.target.checked)}
/>
</label>
<div className="task-body">
<div className="task-title">{e.obligation.title}</div>
<div className="task-meta">
<button className="task-system" onClick={() => onOpenSystem(e.systemId)}>
{e.systemName}
</button>
<span>{e.obligation.articles}</span>
<span>
{t.tasksDue}: {uiDate(e.obligation.deadline, locale)}
</span>
</div>
</div>
</li>
))}
</ul>
)}
</div>
);
}
/**
* Systemübergreifende, abhakbare Aufgabenliste: was erledigt ist,
* was jetzt fällig ist und was noch kommt mobiltauglich.
*/
export function ChecklistView({
systems,
profile,
onToggle,
onOpenSystem,
}: ChecklistViewProps): JSX.Element {
const { t, pack } = useI18n();
const today = todayISO();
const checklist = useMemo(
() => buildChecklist(systems, pack, profile.countries, today),
[systems, pack, profile.countries, today],
);
const pct = checklist.total === 0 ? 0 : Math.round((checklist.resolvedCount / checklist.total) * 100);
return (
<div>
<h1>{t.tasksTitle}</h1>
<p className="sub">{t.tasksSub}</p>
{checklist.total === 0 ? (
<div className="panel">
<div className="empty">{t.tasksEmpty}</div>
</div>
) : (
<>
<div className="panel">
<div className="progress-label">
{fmt(t.tasksProgress, { done: checklist.resolvedCount, total: checklist.total })} {pct} %
</div>
<div className="progress-track">
<div className="progress-fill" style={{ width: `${pct}%` }} />
</div>
</div>
<Group
title={t.tasksDueNow}
entries={checklist.dueNow}
emptyText={t.tasksAllDone}
onToggle={onToggle}
onOpenSystem={onOpenSystem}
/>
<Group
title={t.tasksUpcoming}
entries={checklist.upcoming}
emptyText={t.tasksAllDone}
onToggle={onToggle}
onOpenSystem={onOpenSystem}
/>
<Group
title={t.tasksResolved}
entries={checklist.resolved}
emptyText="—"
onToggle={onToggle}
onOpenSystem={onOpenSystem}
/>
</>
)}
</div>
);
}

View File

@ -1,8 +1,10 @@
import { CONTENT_PACK } from '../content/pack';
import { buildChecklist } from '../engine/checklist';
import { inventoryReport } from '../engine/documents';
import type { AISystem, CompanyProfile } from '../engine/types';
import { useI18n } from '../i18n/context';
import { fmt } from '../i18n/ui';
import { printDocument } from '../print/print';
import { downloadText, RiskBadge, ROLE_LABELS, todayISO } from './common';
import { downloadText, RiskBadge, ROLE_UI, todayISO, uiDate } from './common';
interface DashboardProps {
readonly systems: readonly AISystem[];
@ -13,51 +15,44 @@ interface DashboardProps {
/** Übersichtsseite: Kennzahlen, Fristen und das KI-Inventar. */
export function Dashboard({ systems, profile, onOpen, onNew }: DashboardProps): JSX.Element {
const { t, locale, pack } = useI18n();
const today = todayISO();
const count = (risk: string): number =>
systems.filter((s) => s.classification.riskClass === risk).length;
const openObligations = systems.reduce((acc, s) => {
const done = Object.values(s.obligationStatus).filter(
(st) => st === 'done' || st === 'not-applicable',
).length;
return acc + Math.max(0, Object.keys(s.obligationStatus).length - done);
}, 0);
const checklist = buildChecklist(systems, pack, profile.countries, today);
const openTasks = checklist.total - checklist.resolvedCount;
return (
<div>
<h1>Dashboard</h1>
<p className="sub">
EU-AI-Act-Compliance auf einen Blick. Alle Daten bleiben lokal auf diesem Gerät.
</p>
<h1>{t.dashTitle}</h1>
<p className="sub">{t.dashSub}</p>
<div className="panel grid">
<div className="stat">
<div className="num">{systems.length}</div>
<div className="lbl">KI-Systeme im Inventar</div>
<div className="lbl">{t.statSystems}</div>
</div>
<div className="stat">
<div className="num">{count('high')}</div>
<div className="lbl">Hochrisiko-Systeme</div>
<div className="lbl">{t.statHigh}</div>
</div>
<div className="stat">
<div className="num">{count('prohibited')}</div>
<div className="lbl">Verbotene Praktiken</div>
<div className="lbl">{t.statProhibited}</div>
</div>
<div className="stat">
<div className="num">{openObligations}</div>
<div className="lbl">Offene Pflichten (begonnen)</div>
<div className="num">{openTasks}</div>
<div className="lbl">{t.statOpen}</div>
</div>
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Geltungsfristen des AI Act</h2>
<h2 style={{ marginTop: 0 }}>{t.deadlinesTitle}</h2>
<ul className="deadline-list">
{CONTENT_PACK.deadlines.map((d) => (
{pack.deadlines.map((d) => (
<li key={d.id}>
<span className={`date ${d.date < today ? 'past' : ''}`}>
{d.date.split('-').reverse().join('.')}
</span>
<span className={`date ${d.date < today ? 'past' : ''}`}>{uiDate(d.date, locale)}</span>
<span>
<strong>{d.label}</strong> {d.description}
</span>
@ -67,8 +62,8 @@ export function Dashboard({ systems, profile, onOpen, onNew }: DashboardProps):
</div>
<div className="panel">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ margin: 0 }}>KI-Inventar</h2>
<div className="panel-head">
<h2 style={{ margin: 0 }}>{t.inventoryTitle}</h2>
<div className="toolbar" style={{ margin: 0 }}>
{systems.length > 0 && (
<>
@ -77,19 +72,19 @@ export function Dashboard({ systems, profile, onOpen, onNew }: DashboardProps):
onClick={() =>
printDocument(
`ki-inventar-${today}`,
inventoryReport(systems, CONTENT_PACK, today, profile),
inventoryReport(systems, pack, today, profile, locale),
profile,
)
}
>
Inventar drucken / PDF
{t.inventoryPrint}
</button>
<button
className="ghost"
onClick={() =>
downloadText(
`ki-inventar-${today}.md`,
inventoryReport(systems, CONTENT_PACK, today, profile),
inventoryReport(systems, pack, today, profile, locale),
)
}
>
@ -98,24 +93,22 @@ export function Dashboard({ systems, profile, onOpen, onNew }: DashboardProps):
</>
)}
<button className="primary" onClick={onNew}>
+ System bewerten
{t.newSystem}
</button>
</div>
</div>
{systems.length === 0 ? (
<div className="empty">
Noch keine KI-Systeme erfasst. Starten Sie mit der Bewertung Ihres ersten Systems
z. B. Ihres Chatbots, Ihres Bewerber-Screenings oder eines eingekauften KI-Tools.
</div>
<div className="empty">{t.emptyInventory}</div>
) : (
<div className="table-scroll">
<table>
<thead>
<tr>
<th>System</th>
<th>Rolle</th>
<th>Risikoklasse</th>
<th>Verantwortlich</th>
<th>Zuletzt bewertet</th>
<th>{t.colSystem}</th>
<th>{t.colRole}</th>
<th>{t.colRisk}</th>
<th>{t.colOwner}</th>
<th>{t.colUpdated}</th>
</tr>
</thead>
<tbody>
@ -124,23 +117,21 @@ export function Dashboard({ systems, profile, onOpen, onNew }: DashboardProps):
<td>
<strong>{s.name}</strong>
</td>
<td>{ROLE_LABELS[s.answers.role]}</td>
<td>{ROLE_UI[locale][s.answers.role]}</td>
<td>
<RiskBadge risk={s.classification.riskClass} />
</td>
<td>{s.owner || '—'}</td>
<td>{s.updatedAt.slice(0, 10).split('-').reverse().join('.')}</td>
<td>{uiDate(s.updatedAt.slice(0, 10), locale)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<p className="disclaimer">
Der AI-Act-Kompass ist eine strukturierte Arbeitshilfe auf Basis der Verordnung (EU) 2024/1689
keine Rechtsberatung. Content-Pack v{CONTENT_PACK.version}.
</p>
<p className="disclaimer">{fmt(t.disclaimerLine, { version: pack.version })}</p>
</div>
);
}

View File

@ -0,0 +1,47 @@
import { useState } from 'react';
import { useI18n } from '../i18n/context';
const ACK_KEY = 'ai-act-kompass/legal-ack/v1';
function hasAcknowledged(): boolean {
try {
return localStorage.getItem(ACK_KEY) !== null;
} catch {
return false;
}
}
/**
* Rechtlicher Hinweis beim ersten Start: Die Nutzung ist erst nach
* ausdrücklicher Bestätigung möglich, dass das Tool nur eine Arbeitshilfe
* und keine Rechtsberatung ist. Die Bestätigung wird mit Zeitstempel
* lokal gespeichert.
*/
export function DisclaimerGate(): JSX.Element | null {
const { t } = useI18n();
const [acknowledged, setAcknowledged] = useState<boolean>(() => hasAcknowledged());
if (acknowledged) return null;
return (
<div className="gate-backdrop" role="dialog" aria-modal="true" aria-label={t.legalGateTitle}>
<div className="gate">
<h1>{t.legalGateTitle}</h1>
<p>{t.legalGateBody1}</p>
<p>
<strong>{t.legalGateBody2}</strong>
</p>
<p>{t.legalGateBody3}</p>
<button
className="primary"
onClick={() => {
localStorage.setItem(ACK_KEY, new Date().toISOString());
setAcknowledged(true);
}}
>
{t.legalGateAccept}
</button>
</div>
</div>
);
}

View File

@ -1,6 +1,8 @@
import { useRef, useState } from 'react';
import { CONTENT_PACK } from '../content/pack';
import type { AISystem, CompanyProfile } from '../engine/types';
import { useI18n } from '../i18n/context';
import { LOCALES } from '../i18n/types';
import { fmt } from '../i18n/ui';
import { clearAll, exportData, importData } from '../store/storage';
import { downloadText, todayISO } from './common';
@ -14,17 +16,9 @@ interface SettingsViewProps {
const MAX_LOGO_BYTES = 300_000;
const PROFILE_FIELDS: readonly { key: keyof CompanyProfile; label: string; placeholder: string }[] = [
{ key: 'name', label: 'Firmenname', placeholder: 'Muster GmbH' },
{ key: 'street', label: 'Straße & Hausnummer', placeholder: 'Musterstraße 1' },
{ key: 'zipCity', label: 'PLZ & Ort', placeholder: '60311 Frankfurt am Main' },
{ key: 'contactPerson', label: 'Ansprechpartner', placeholder: 'Max Mustermann' },
{ key: 'email', label: 'E-Mail', placeholder: 'compliance@muster.de' },
{ key: 'phone', label: 'Telefon', placeholder: '+49 69 000000' },
{ key: 'website', label: 'Website', placeholder: 'www.muster.de' },
];
type ProfileTextKey = Exclude<keyof CompanyProfile, 'logoDataUrl' | 'countries'>;
/** Einstellungen: Firmenprofil (Briefkopf), Datensicherung, Löschen, Content-Pack-Info. */
/** Einstellungen: Sprache, Firmenprofil, Länder-Module, Backup, Rechtshinweis. */
export function SettingsView({
systems,
profile,
@ -32,52 +26,110 @@ export function SettingsView({
onImport,
onClear,
}: SettingsViewProps): JSX.Element {
const { t, locale, pack, setLocale } = useI18n();
const fileInput = useRef<HTMLInputElement>(null);
const logoInput = useRef<HTMLInputElement>(null);
const [message, setMessage] = useState<string>('');
const [logoMessage, setLogoMessage] = useState<string>('');
const profileFields: readonly { key: ProfileTextKey; label: string; placeholder: string }[] = [
{ key: 'name', label: t.pfName, placeholder: 'Muster GmbH' },
{ key: 'street', label: t.pfStreet, placeholder: 'Musterstraße 1' },
{ key: 'zipCity', label: t.pfZip, placeholder: '60311 Frankfurt am Main' },
{ key: 'contactPerson', label: t.pfContact, placeholder: 'Max Mustermann' },
{ key: 'email', label: t.pfEmail, placeholder: 'compliance@muster.de' },
{ key: 'phone', label: t.pfPhone, placeholder: '+49 69 000000' },
{ key: 'website', label: t.pfWeb, placeholder: 'www.muster.de' },
];
const handleImport = async (file: File): Promise<void> => {
try {
const result = importData(await file.text());
onImport(result.systems, result.profile);
setMessage(`Import abgeschlossen: ${result.systems.length} System(e)${result.profile ? ' inkl. Firmenprofil' : ''}.`);
setMessage(
fmt(t.importOk, {
count: result.systems.length,
profile: result.profile ? t.importProfileSuffix : '',
}),
);
} catch (err) {
setMessage(`Import fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`);
setMessage(fmt(t.importFail, { error: err instanceof Error ? err.message : String(err) }));
}
};
const handleLogo = (file: File): void => {
if (!file.type.startsWith('image/')) {
setLogoMessage('Bitte eine Bilddatei wählen (PNG, JPG oder SVG).');
setLogoMessage(t.logoNotImage);
return;
}
const reader = new FileReader();
reader.onload = () => {
const dataUrl = typeof reader.result === 'string' ? reader.result : '';
if (dataUrl.length > MAX_LOGO_BYTES * 1.4) {
setLogoMessage('Logo zu groß — bitte eine Datei unter 300 KB verwenden.');
setLogoMessage(t.logoTooBig);
return;
}
onProfileChange({ ...profile, logoDataUrl: dataUrl });
setLogoMessage('Logo übernommen.');
setLogoMessage(t.logoTaken);
};
reader.readAsDataURL(file);
};
const toggleCountry = (code: string): void => {
const countries = profile.countries.includes(code)
? profile.countries.filter((c) => c !== code)
: [...profile.countries, code];
onProfileChange({ ...profile, countries });
};
return (
<div>
<h1>Einstellungen</h1>
<p className="sub">Datenhoheit: Alles liegt lokal in diesem Browser kein Cloud-Sync, keine Telemetrie.</p>
<h1>{t.settingsTitle}</h1>
<p className="sub">{t.settingsSub}</p>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Firmenprofil (Briefkopf für Berichte & PDF-Druck)</h2>
<p className="hint">
Diese Angaben erscheinen im Kopf und in der Fußzeile aller generierten und gedruckten
Dokumente. Das Logo bleibt lokal gespeichert und verlässt das Gerät nicht.
</p>
<h2 style={{ marginTop: 0 }}>{t.langTitle}</h2>
<div className="toolbar">
{LOCALES.map((l) => (
<button
key={l}
className={l === locale ? 'primary' : 'ghost'}
onClick={() => setLocale(l)}
>
{l === 'de' ? 'Deutsch' : 'English'}
</button>
))}
</div>
<p className="hint">{t.langHint}</p>
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>{t.countriesTitle}</h2>
<p className="hint">{t.countriesHint}</p>
{pack.national.map((n) => (
<label key={n.code} className="check">
<input
type="checkbox"
checked={profile.countries.includes(n.code)}
onChange={() => toggleCountry(n.code)}
/>
<span>
<strong>
{n.code} {n.country}
</strong>
<span className="ref">
{t.authorityLabel}: {n.authority}
</span>
</span>
</label>
))}
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>{t.profileTitle}</h2>
<p className="hint">{t.profileHint}</p>
<div className="profile-grid">
{PROFILE_FIELDS.map((f) => (
{profileFields.map((f) => (
<label key={f.key} className="field">
<span>{f.label}</span>
<input
@ -91,20 +143,17 @@ export function SettingsView({
</div>
<div className="logo-row">
{profile.logoDataUrl ? (
<img className="logo-preview" src={profile.logoDataUrl} alt="Firmenlogo" />
<img className="logo-preview" src={profile.logoDataUrl} alt="Logo" />
) : (
<span className="hint">Kein Logo hinterlegt.</span>
<span className="hint">{t.logoNone}</span>
)}
<div className="toolbar" style={{ margin: 0 }}>
<button className="ghost" onClick={() => logoInput.current?.click()}>
Logo hochladen
{t.logoUpload}
</button>
{profile.logoDataUrl && (
<button
className="danger"
onClick={() => onProfileChange({ ...profile, logoDataUrl: '' })}
>
Logo entfernen
<button className="danger" onClick={() => onProfileChange({ ...profile, logoDataUrl: '' })}>
{t.logoRemove}
</button>
)}
</div>
@ -124,21 +173,17 @@ export function SettingsView({
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Regulatorischer Content-Pack</h2>
<h2 style={{ marginTop: 0 }}>{t.packTitle}</h2>
<p style={{ fontSize: 14 }}>
Version <strong>{CONTENT_PACK.version}</strong> · Stand {CONTENT_PACK.releasedAt}
Version <strong>{pack.version}</strong> · {t.packStand} {pack.releasedAt}
<br />
<span className="hint">{CONTENT_PACK.legalBasis}</span>
</p>
<p className="hint">
Der Content-Pack (Verbote, Annex-III-Bereiche, Pflichten, Fristen) ist als austauschbares
Datenpaket modelliert. Updates erscheinen mit neuen App-Versionen bzw. künftig über einen
signierten Update-Feed.
<span className="hint">{pack.legalBasis}</span>
</p>
<p className="hint">{t.packHint}</p>
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Datensicherung</h2>
<h2 style={{ marginTop: 0 }}>{t.backupTitle}</h2>
<div className="toolbar">
<button
className="primary"
@ -150,10 +195,10 @@ export function SettingsView({
)
}
>
Backup exportieren (JSON)
{t.backupExport}
</button>
<button className="ghost" onClick={() => fileInput.current?.click()}>
Backup importieren
{t.backupImport}
</button>
<input
ref={fileInput}
@ -171,19 +216,28 @@ export function SettingsView({
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Alle Daten löschen</h2>
<p className="hint">Entfernt alle erfassten KI-Systeme und das Firmenprofil unwiderruflich aus diesem Browser.</p>
<h2 style={{ marginTop: 0 }}>{t.legalNoticeTitle}</h2>
<p className="hint">{t.legalGateBody1}</p>
<p className="hint">
<strong>{t.legalGateBody2}</strong>
</p>
<p className="hint">{t.legalGateBody3}</p>
</div>
<div className="panel">
<h2 style={{ marginTop: 0 }}>{t.deleteTitle}</h2>
<p className="hint">{t.deleteHint}</p>
<button
className="danger"
onClick={() => {
if (window.confirm('Wirklich alle lokalen Daten unwiderruflich löschen?')) {
if (window.confirm(t.deleteConfirm)) {
clearAll();
onClear();
setMessage('Alle Daten gelöscht.');
setMessage(t.deleteDone);
}
}}
>
Alles löschen
{t.deleteBtn}
</button>
</div>
</div>

View File

@ -1,10 +1,13 @@
import { useMemo, useState } from 'react';
import { CONTENT_PACK } from '../content/pack';
import { docStrings } from '../engine/docStrings';
import { actionPlan, annexIVSkeleton, riskReport } from '../engine/documents';
import { conformityDeclaration, procurementSpec } from '../engine/extraDocs';
import { deriveObligations } from '../engine/obligations';
import type { AISystem, CompanyProfile, ObligationStatus } from '../engine/types';
import { useI18n } from '../i18n/context';
import { fmt } from '../i18n/ui';
import { printDocument } from '../print/print';
import { downloadText, RiskBadge, ROLE_LABELS, todayISO } from './common';
import { downloadText, RiskBadge, ROLE_UI, todayISO, uiDate } from './common';
interface SystemDetailProps {
readonly system: AISystem;
@ -15,14 +18,7 @@ interface SystemDetailProps {
readonly onBack: () => void;
}
const STATUS_OPTIONS: readonly { value: ObligationStatus; label: string }[] = [
{ value: 'open', label: 'Offen' },
{ value: 'in-progress', label: 'In Umsetzung' },
{ value: 'done', label: 'Erledigt' },
{ value: 'not-applicable', label: 'Nicht anwendbar' },
];
type DocKind = 'report' | 'plan' | 'annexIV';
type DocKind = 'report' | 'plan' | 'annexIV' | 'procurement' | 'conformity';
/** Detailansicht eines Systems: Klassifizierung, Pflichten-Checkliste, Dokumente. */
export function SystemDetail({
@ -33,14 +29,22 @@ export function SystemDetail({
onDelete,
onBack,
}: SystemDetailProps): JSX.Element {
const { t, locale, pack } = useI18n();
const today = todayISO();
const [preview, setPreview] = useState<DocKind | null>(null);
const obligations = useMemo(
() => deriveObligations(system.classification, system.answers, CONTENT_PACK),
[system],
() => deriveObligations(system.classification, system.answers, pack, profile.countries),
[system, pack, profile.countries],
);
const statusOptions: readonly { value: ObligationStatus; label: string }[] = [
{ value: 'open', label: t.statusOpen },
{ value: 'in-progress', label: t.statusInProgress },
{ value: 'done', label: t.statusDone },
{ value: 'not-applicable', label: t.statusNA },
];
const setStatus = (obligationId: string, status: ObligationStatus): void => {
onUpdate({
...system,
@ -51,38 +55,58 @@ export function SystemDetail({
const docs: Record<DocKind, { label: string; file: string; render: () => string }> = {
report: {
label: 'Risikobericht',
label: t.docRisk,
file: `risikobericht-${system.name}-${today}.md`,
render: () => riskReport(system, CONTENT_PACK, today, profile),
render: () => riskReport(system, pack, today, profile, locale),
},
plan: {
label: 'Maßnahmenplan',
label: t.docPlan,
file: `massnahmenplan-${system.name}-${today}.md`,
render: () => actionPlan(system, obligations, CONTENT_PACK, today, profile),
render: () => actionPlan(system, obligations, pack, today, profile, locale),
},
annexIV: {
label: 'Annex-IV-Dokumentation',
label: t.docAnnexIV,
file: `annex-iv-${system.name}-${today}.md`,
render: () => annexIVSkeleton(system, CONTENT_PACK, today, profile),
render: () => annexIVSkeleton(system, pack, today, profile, locale),
},
procurement: {
label: t.docProcurement,
file: `lastenheft-${system.name}-${today}.md`,
render: () => procurementSpec(system, pack, today, profile, locale),
},
conformity: {
label: t.docConformity,
file: `konformitaetserklaerung-${system.name}-${today}.md`,
render: () => conformityDeclaration(system, pack, today, profile, locale),
},
};
const showAnnexIV = system.classification.riskClass === 'high' && system.answers.role === 'provider';
const isProvider = system.answers.role === 'provider';
const isHigh = system.classification.riskClass === 'high';
const visibleDocs = (Object.keys(docs) as DocKind[]).filter((kind) => {
if (kind === 'annexIV' || kind === 'conformity') return isHigh && isProvider;
if (kind === 'procurement') return system.answers.role === 'deployer';
return true;
});
const nationalCodes = profile.countries.filter((code) =>
pack.national.some((n) => n.code === code),
);
return (
<div>
<button className="ghost" onClick={onBack}>
Zurück zum Dashboard
{t.detailBack}
</button>
<h1 style={{ marginTop: 14 }}>
{system.name} <RiskBadge risk={system.classification.riskClass} />
</h1>
<p className="sub">
{ROLE_LABELS[system.answers.role]} · {system.purpose || 'Keine Zweckbestimmung erfasst'}
{ROLE_UI[locale][system.answers.role]} · {system.purpose || t.detailNoPurpose}
</p>
<div className="panel">
<h2 style={{ marginTop: 0 }}>Begründung der Einstufung</h2>
<h2 style={{ marginTop: 0 }}>{t.detailReasons}</h2>
<ul className="reasons">
{system.classification.reasons.map((r) => (
<li key={r}>{r}</li>
@ -90,24 +114,30 @@ export function SystemDetail({
</ul>
<div className="toolbar">
<button className="ghost" onClick={onReassess}>
Neu bewerten
{t.detailReassess}
</button>
<button className="danger" onClick={onDelete}>
System löschen
{t.detailDelete}
</button>
</div>
</div>
{obligations.length > 0 && (
<div className="panel">
<h2 style={{ marginTop: 0 }}>Pflichten ({obligations.length})</h2>
<h2 style={{ marginTop: 0 }}>
{t.detailObligations} ({obligations.length})
{nationalCodes.length > 0 && (
<span className="hint"> {fmt(t.detailNational, { codes: nationalCodes.join(', ') })}</span>
)}
</h2>
<div className="table-scroll">
<table>
<thead>
<tr>
<th>Pflicht</th>
<th>Rechtsgrundlage</th>
<th>Frist</th>
<th>Status</th>
<th>{docStrings(locale).plan.colObligation}</th>
<th>{docStrings(locale).plan.colLegal}</th>
<th>{docStrings(locale).plan.colDeadline}</th>
<th>{docStrings(locale).plan.colStatus}</th>
</tr>
</thead>
<tbody>
@ -120,13 +150,13 @@ export function SystemDetail({
<div className="hint">{ob.description}</div>
</td>
<td>{ob.articles}</td>
<td>{ob.deadline.split('-').reverse().join('.')}</td>
<td>{uiDate(ob.deadline, locale)}</td>
<td>
<select
value={status}
onChange={(e) => setStatus(ob.id, e.target.value as ObligationStatus)}
>
{STATUS_OPTIONS.map((o) => (
{statusOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
@ -139,28 +169,26 @@ export function SystemDetail({
</tbody>
</table>
</div>
</div>
)}
<div className="panel">
<h2 style={{ marginTop: 0 }}>Dokumente generieren</h2>
<h2 style={{ marginTop: 0 }}>{t.detailDocs}</h2>
<div className="toolbar">
{(Object.keys(docs) as DocKind[])
.filter((k) => k !== 'annexIV' || showAnnexIV)
.map((kind) => (
<span key={kind} style={{ display: 'flex', gap: 4 }}>
{visibleDocs.map((kind) => (
<span key={kind} style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
<button className="ghost" onClick={() => setPreview(preview === kind ? null : kind)}>
{docs[kind].label} {preview === kind ? 'ausblenden' : 'ansehen'}
{docs[kind].label} {preview === kind ? t.docHide : t.docShow}
</button>
<button
className="primary"
onClick={() => printDocument(docs[kind].file.replace(/\.md$/, ''), docs[kind].render(), profile)}
onClick={() =>
printDocument(docs[kind].file.replace(/\.md$/, ''), docs[kind].render(), profile)
}
>
Drucken / PDF
{t.docPrint}
</button>
<button
className="ghost"
onClick={() => downloadText(docs[kind].file, docs[kind].render())}
>
<button className="ghost" onClick={() => downloadText(docs[kind].file, docs[kind].render())}>
.md
</button>
</span>

View File

@ -1,10 +1,9 @@
import { useMemo, useState } from 'react';
import { CONTENT_PACK } from '../content/pack';
import { classify } from '../engine/classify';
import type { AISystem, AssessmentAnswers, ContentItem, Role } from '../engine/types';
import { RiskBadge, ROLE_LABELS } from './common';
const STEPS = ['Grunddaten', 'KI-Definition', 'Verbote', 'Hochrisiko', 'Transparenz & GPAI', 'Ergebnis'] as const;
import { useI18n } from '../i18n/context';
import { fmt } from '../i18n/ui';
import { RiskBadge, ROLE_UI } from './common';
interface WizardProps {
/** Bestehendes System bei Neubewertung, sonst null. */
@ -29,15 +28,12 @@ interface Draft {
transparencyTriggers: readonly string[];
}
function initialDraft(existing: AISystem | null): Draft {
function initialDraft(existing: AISystem | null, allDefIds: readonly string[]): Draft {
if (!existing) {
return {
name: '',
purpose: '',
vendor: '',
owner: '',
name: '', purpose: '', vendor: '', owner: '',
role: 'deployer',
defCriteria: CONTENT_PACK.aiDefinitionCriteria.map((c) => c.id),
defCriteria: allDefIds,
isGPAIModel: false,
prohibitedPractices: [],
annexIIICategories: [],
@ -54,7 +50,7 @@ function initialDraft(existing: AISystem | null): Draft {
vendor: existing.vendor,
owner: existing.owner,
role: a.role,
defCriteria: a.isAISystem ? CONTENT_PACK.aiDefinitionCriteria.map((c) => c.id) : [],
defCriteria: a.isAISystem ? allDefIds : [],
isGPAIModel: a.isGPAIModel,
prohibitedPractices: a.prohibitedPractices,
annexIIICategories: a.annexIIICategories,
@ -102,10 +98,14 @@ function CheckList({
/** Mehrstufiger Bewertungs-Wizard: erfasst ein KI-System und klassifiziert es. */
export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element {
const [step, setStep] = useState(0);
const [draft, setDraft] = useState<Draft>(() => initialDraft(existing));
const { t, locale, pack } = useI18n();
const steps = [t.stepBasics, t.stepDefinition, t.stepProhibited, t.stepHighRisk, t.stepTransparency, t.stepResult];
const allDefIds = useMemo(() => pack.aiDefinitionCriteria.map((c) => c.id), [pack]);
const isAISystem = draft.defCriteria.length === CONTENT_PACK.aiDefinitionCriteria.length;
const [step, setStep] = useState(0);
const [draft, setDraft] = useState<Draft>(() => initialDraft(existing, allDefIds));
const isAISystem = draft.defCriteria.length === allDefIds.length;
const answers: AssessmentAnswers = useMemo(
() => ({
@ -122,7 +122,7 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
[draft, isAISystem],
);
const classification = useMemo(() => classify(answers, CONTENT_PACK), [answers]);
const classification = useMemo(() => classify(answers, pack, locale), [answers, pack, locale]);
const canNext = step !== 0 || draft.name.trim().length > 0;
@ -144,11 +144,11 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
return (
<div>
<h1>{existing ? `Neubewertung: ${existing.name}` : 'Neues KI-System bewerten'}</h1>
<p className="sub">Beantworten Sie die Fragen die Risikoklasse wird automatisch nach der Systematik des AI Act ermittelt.</p>
<h1>{existing ? fmt(t.wizardTitleRe, { name: existing.name }) : t.wizardTitleNew}</h1>
<p className="sub">{t.wizardSub}</p>
<div className="wizard-steps">
{STEPS.map((label, i) => (
{steps.map((label, i) => (
<span key={label} className={`step ${i === step ? 'current' : i < step ? 'done' : ''}`}>
{i + 1}. {label}
</span>
@ -159,70 +159,64 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
{step === 0 && (
<>
<label className="field">
<span>Name des Systems *</span>
<span>{t.fieldName}</span>
<input
type="text"
value={draft.name}
placeholder="z. B. Bewerber-Screening-Tool"
placeholder={t.fieldNamePh}
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
/>
</label>
<label className="field">
<span>Zweckbestimmung</span>
<span>{t.fieldPurpose}</span>
<textarea
rows={2}
value={draft.purpose}
placeholder="Wofür wird das System eingesetzt?"
placeholder={t.fieldPurposePh}
onChange={(e) => setDraft({ ...draft, purpose: e.target.value })}
/>
</label>
<label className="field">
<span>Hersteller / Anbieter</span>
<span>{t.fieldVendor}</span>
<input type="text" value={draft.vendor} onChange={(e) => setDraft({ ...draft, vendor: e.target.value })} />
</label>
<label className="field">
<span>Verantwortliche Person (intern)</span>
<span>{t.fieldOwner}</span>
<input type="text" value={draft.owner} onChange={(e) => setDraft({ ...draft, owner: e.target.value })} />
</label>
<label className="field">
<span>Rolle Ihres Unternehmens</span>
<span>{t.fieldRole}</span>
<select value={draft.role} onChange={(e) => setDraft({ ...draft, role: e.target.value as Role })}>
{(Object.keys(ROLE_LABELS) as Role[]).map((r) => (
{(Object.keys(ROLE_UI[locale]) as Role[]).map((r) => (
<option key={r} value={r}>
{ROLE_LABELS[r]}
{ROLE_UI[locale][r]}
</option>
))}
</select>
<span className="hint">
Anbieter = entwickelt/vertreibt das System unter eigenem Namen. Betreiber = setzt ein fremdes System ein (häufigster KMU-Fall).
</span>
<span className="hint">{t.roleHint}</span>
</label>
</>
)}
{step === 1 && (
<>
<h2>Erfüllt das System die KI-Definition (Art. 3 Nr. 1)?</h2>
<p className="hint">Nur wenn alle Kriterien zutreffen, fällt das System unter den AI Act.</p>
<h2>{t.defTitle}</h2>
<p className="hint">{t.defHint}</p>
<CheckList
items={CONTENT_PACK.aiDefinitionCriteria}
items={pack.aiDefinitionCriteria}
selected={draft.defCriteria}
onToggle={(id) => setDraft({ ...draft, defCriteria: toggle(draft.defCriteria, id) })}
/>
{!isAISystem && (
<p className="hint">
Nicht alle Kriterien erfüllt das System gilt nicht als KI-System, der AI Act ist nicht anwendbar.
</p>
)}
{!isAISystem && <p className="hint"> {t.defWarn}</p>}
</>
)}
{step === 2 && (
<>
<h2>Verbotene Praktiken (Art. 5)</h2>
<p className="hint">Trifft einer dieser Punkte zu, ist der Einsatz seit 2. Februar 2025 verboten.</p>
<h2>{t.prohTitle}</h2>
<p className="hint">{t.prohHint}</p>
<CheckList
items={CONTENT_PACK.prohibitedPractices}
items={pack.prohibitedPractices}
selected={draft.prohibitedPractices}
onToggle={(id) => setDraft({ ...draft, prohibitedPractices: toggle(draft.prohibitedPractices, id) })}
/>
@ -231,7 +225,7 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
{step === 3 && (
<>
<h2>Hochrisiko-Einstufung (Art. 6)</h2>
<h2>{t.hrTitle}</h2>
<label className="check">
<input
type="checkbox"
@ -239,19 +233,19 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
onChange={(e) => setDraft({ ...draft, isAnnexISafetyComponent: e.target.checked })}
/>
<span>
Das System ist Sicherheitsbauteil eines regulierten Produkts (z. B. Medizinprodukt, Maschine, Aufzug) oder selbst ein solches Produkt.
{t.annexILabel}
<span className="ref">Art. 6 Abs. 1, Annex I</span>
</span>
</label>
<h2>Einsatzbereiche nach Annex III</h2>
<h2>{t.annexIIITitle}</h2>
<CheckList
items={CONTENT_PACK.annexIIICategories}
items={pack.annexIIICategories}
selected={draft.annexIIICategories}
onToggle={(id) => setDraft({ ...draft, annexIIICategories: toggle(draft.annexIIICategories, id) })}
/>
{draft.annexIIICategories.length > 0 && (
<>
<h2>Mögliche Ausnahme (Art. 6 Abs. 3)</h2>
<h2>{t.exceptionTitle}</h2>
<label className="check">
<input
type="checkbox"
@ -259,8 +253,8 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
onChange={(e) => setDraft({ ...draft, involvesProfiling: e.target.checked })}
/>
<span>
Das System führt Profiling natürlicher Personen durch.
<span className="ref">Profiling schließt die Ausnahme aus immer Hochrisiko.</span>
{t.profilingLabel}
<span className="ref">{t.profilingRef}</span>
</span>
</label>
<label className="check">
@ -270,8 +264,8 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
onChange={(e) => setDraft({ ...draft, art63Exception: e.target.checked })}
/>
<span>
Das System erfüllt nur eine eng begrenzte verfahrenstechnische oder vorbereitende Aufgabe bzw. verbessert lediglich das Ergebnis einer zuvor abgeschlossenen menschlichen Tätigkeit ohne wesentliches Risiko für Gesundheit, Sicherheit oder Grundrechte.
<span className="ref">Art. 6 Abs. 3 die Einschätzung muss dokumentiert werden (Art. 6 Abs. 4).</span>
{t.art63Label}
<span className="ref">{t.art63Ref}</span>
</span>
</label>
</>
@ -281,13 +275,13 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
{step === 4 && (
<>
<h2>Transparenzpflichten (Art. 50)</h2>
<h2>{t.trTitle}</h2>
<CheckList
items={CONTENT_PACK.transparencyTriggers}
items={pack.transparencyTriggers}
selected={draft.transparencyTriggers}
onToggle={(id) => setDraft({ ...draft, transparencyTriggers: toggle(draft.transparencyTriggers, id) })}
/>
<h2>General-Purpose AI</h2>
<h2>{t.gpaiTitle}</h2>
<label className="check">
<input
type="checkbox"
@ -295,8 +289,8 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
onChange={(e) => setDraft({ ...draft, isGPAIModel: e.target.checked })}
/>
<span>
Wir stellen ein eigenes GPAI-Modell bereit (z. B. selbst trainiertes oder wesentlich verändertes Basismodell).
<span className="ref">Art. 51 ff. die bloße Nutzung von z. B. GPT/Claude über API zählt nicht.</span>
{t.gpaiLabel}
<span className="ref">{t.gpaiRef}</span>
</span>
</label>
</>
@ -305,19 +299,16 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
{step === 5 && (
<>
<h2>
Ergebnis: <RiskBadge risk={classification.riskClass} />
{t.resultTitle} <RiskBadge risk={classification.riskClass} />
</h2>
<ul className="reasons">
{classification.reasons.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
{classification.gpaiObligations && <p className="hint">Zusätzlich gelten GPAI-Modellpflichten (Art. 53 ff.).</p>}
{classification.riskClass === 'prohibited' && (
<p className="hint">
Der Einsatz dieses Systems ist nach Art. 5 verboten (Bußgeld bis 35 Mio. oder 7 % des weltweiten Jahresumsatzes). Einsatz einstellen und rechtlichen Rat einholen.
</p>
)}
{classification.gpaiObligations && <p className="hint">{t.resultGpai}</p>}
{classification.riskClass === 'prohibited' && <p className="hint"> {t.resultProhibited}</p>}
<p className="disclaimer">{t.resultDisclaimer}</p>
</>
)}
</div>
@ -325,27 +316,27 @@ export function Wizard({ existing, onSave, onCancel }: WizardProps): JSX.Element
<div className="wizard-nav">
<div>
<button className="ghost" onClick={onCancel}>
Abbrechen
{t.btnCancel}
</button>
</div>
<div style={{ display: 'flex', gap: 8 }}>
{step > 0 && (
<button className="ghost" onClick={() => setStep(step - 1)}>
Zurück
{t.btnBack}
</button>
)}
{step < STEPS.length - 1 && (
{step < steps.length - 1 && (
<button
className="primary"
disabled={!canNext}
onClick={() => setStep(isAISystem || step === 0 ? step + 1 : STEPS.length - 1)}
onClick={() => setStep(isAISystem || step === 0 ? step + 1 : steps.length - 1)}
>
Weiter
{t.btnNext}
</button>
)}
{step === STEPS.length - 1 && (
{step === steps.length - 1 && (
<button className="primary" disabled={draft.name.trim().length === 0} onClick={save}>
Speichern
{t.btnSave}
</button>
)}
</div>

View File

@ -1,25 +1,35 @@
import { useI18n } from '../i18n/context';
import type { Locale } from '../i18n/types';
import type { RiskClass, Role } from '../engine/types';
/** Deutsche Anzeige-Labels für Risikoklassen. */
export const RISK_LABELS: Readonly<Record<RiskClass, string>> = {
/** Kurze UI-Labels für Risikoklassen je Sprache. */
export const RISK_UI: Readonly<Record<Locale, Readonly<Record<RiskClass, string>>>> = {
de: {
'out-of-scope': 'Nicht anwendbar',
prohibited: 'Verboten',
high: 'Hochrisiko',
limited: 'Begrenztes Risiko',
minimal: 'Minimales Risiko',
},
en: {
'out-of-scope': 'Out of scope',
prohibited: 'Prohibited',
high: 'High-risk',
limited: 'Limited risk',
minimal: 'Minimal risk',
},
};
/** Deutsche Anzeige-Labels für Rollen. */
export const ROLE_LABELS: Readonly<Record<Role, string>> = {
provider: 'Anbieter',
deployer: 'Betreiber',
importer: 'Einführer',
distributor: 'Händler',
/** Kurze UI-Labels für Rollen je Sprache. */
export const ROLE_UI: Readonly<Record<Locale, Readonly<Record<Role, string>>>> = {
de: { provider: 'Anbieter', deployer: 'Betreiber', importer: 'Einführer', distributor: 'Händler' },
en: { provider: 'Provider', deployer: 'Deployer', importer: 'Importer', distributor: 'Distributor' },
};
/** Farbiges Badge für die Risikoklasse eines Systems. */
/** Farbiges Stempel-Badge für die Risikoklasse eines Systems. */
export function RiskBadge({ risk }: { readonly risk: RiskClass }): JSX.Element {
return <span className={`badge ${risk}`}>{RISK_LABELS[risk]}</span>;
const { locale } = useI18n();
return <span className={`badge ${risk}`}>{RISK_UI[locale][risk]}</span>;
}
/**
@ -43,3 +53,9 @@ export function downloadText(filename: string, content: string, mime = 'text/mar
export function todayISO(): string {
return new Date().toISOString().slice(0, 10);
}
/** Formatiert ein ISO-Datum für die UI (DE: TT.MM.JJJJ, EN: ISO). */
export function uiDate(iso: string, locale: Locale): string {
if (locale === 'en') return iso;
return iso.split('-').reverse().join('.');
}