Files
breakpilot-compliance/admin-compliance/app/sdk/cra/_components/DatasheetExtract.tsx
T
Benjamin Admin 9e2655bfef
CI / nodejs-build (push) Successful in 3m10s
CI / secret-scan (push) Has been skipped
CI / test-go (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / detect-changes (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 12s
CI / test-python-backend (push) Successful in 32s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 24s
CI / go-lint (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
fix(cra): IACE-Create id-Wrapper + MaschinenVO eigene Sektion
1) createProject las proj.id, der Create-Response ist aber {project:{id}} →
   'Projekt anlegen' war kaputt. Jetzt proj.project?.id. E2E verifiziert
   (create→put limits_form→get→delete = 200).
2) MaschinenVO-Sicherheitspflichten wurden in die CRA-Cyber-Buckets
   (Code/Prozess/Doku) gemischt → fehl-kategorisiert (Maschinen-Safety ≠
   CRA-Annex-I-Cyber). Jetzt eigene Response-Liste machinery_guideline +
   eigener Frontend-Abschnitt 'Maschinensicherheit (MaschinenVO 2023/1230)';
   geklebtes 'MaschVO'-Badge entfaellt damit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 00:12:52 +02:00

187 lines
9.2 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { DATASHEET_EXAMPLES } from './readiness-presets'
interface Followup { key: string; label: string; question: string }
interface ExtractResult {
limits: Record<string, string>
provenance: Record<string, string>
detected: { interfaces: string[]; units: string[] }
llm_status?: string
filled: string[]
missing: string[]
followup: Followup[]
}
const FIELD_LABEL: Record<string, string> = {
machine_designation: 'Maschinenbezeichnung', machine_type: 'Maschinentyp', manufacturer: 'Hersteller',
year_of_construction: 'Baujahr', general_description: 'Allgemeine Beschreibung',
intended_purpose: 'Verwendungszweck', area_of_use: 'Einsatzbereich', operating_modes: 'Betriebsarten',
variants: 'Varianten', foreseeable_misuses: 'Vorhersehbare Fehlanwendungen',
spatial_limits: 'Räumliche Grenzen', temporal_limits: 'Zeitliche Grenzen',
operating_conditions: 'Betriebsbedingungen', energy_supply: 'Energieversorgung',
mechanical_interfaces: 'Mechanische Schnittstellen', electrical_interfaces: 'Elektrische Schnittstellen',
software_interfaces: 'Software-Schnittstellen', pneumatic_hydraulic_interfaces: 'Pneumatik/Hydraulik',
person_groups: 'Personengruppen', qualification_requirements: 'Qualifikationsanforderungen',
}
export function DatasheetExtract() {
const router = useRouter()
const [text, setText] = useState('')
const [res, setRes] = useState<ExtractResult | null>(null)
const [loading, setLoading] = useState(false)
const [creating, setCreating] = useState(false)
const [answers, setAnswers] = useState<Record<string, string>>({})
const run = async () => {
setLoading(true)
try {
const r = await fetch('/api/v1/cra/extract-datasheet', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
})
setRes(r.ok ? await r.json() : null)
setAnswers({})
} finally { setLoading(false) }
}
// Create an IACE project from the extracted limits + follow-up answers. The
// limits land as the project's editable limits_form; the interview form stays
// fully editable (every prefilled field can be changed). Manual creation via
// /sdk/iace remains unchanged.
const createProject = async () => {
if (!res) return
setCreating(true)
try {
const nonEmptyAnswers = Object.fromEntries(
Object.entries(answers).filter(([, v]) => (v || '').trim()),
)
const limits = { ...res.limits, ...nonEmptyAnswers }
const machineName = limits.machine_designation || limits.machine_type || 'Neues Produkt'
const cr = await fetch('/api/sdk/v1/iace/projects', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
machine_name: machineName,
machine_type: limits.machine_type || 'Nicht angegeben',
manufacturer: limits.manufacturer || 'Nicht angegeben',
}),
})
if (!cr.ok) return
const proj = await cr.json()
const pid = proj.project?.id || proj.id || proj.project_id
if (!pid) return
await fetch(`/api/sdk/v1/iace/projects/${pid}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ metadata: { limits_form: limits } }),
})
router.push(`/sdk/iace/${pid}/interview`)
} finally { setCreating(false) }
}
return (
<div className="rounded-xl border border-indigo-200 dark:border-indigo-800 bg-indigo-50/50 dark:bg-indigo-900/20 p-5 mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Datenblatt-Analyse Maschinengrenzen</h2>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">
Datenblatt einfügen wir füllen die ISO-12100-Grenzen automatisch (lokales KI-Modell, Datenhoheit)
und fragen gezielt nach, was nicht im Datenblatt steht. Jeder übernommene Wert trägt seine Quelle.
</p>
<div className="flex flex-wrap items-center gap-2 mt-3">
<span className="text-xs text-gray-500">Beispiel laden:</span>
{DATASHEET_EXAMPLES.map((d) => (
<button key={d.id} onClick={() => { setText(d.text); setRes(null) }}
className="rounded border border-indigo-300 dark:border-indigo-700 bg-white dark:bg-gray-800 text-indigo-700 dark:text-indigo-300 text-xs px-2 py-1 hover:bg-indigo-100">
{d.label}
</button>
))}
</div>
<textarea
value={text} onChange={(e) => setText(e.target.value)}
placeholder="Datenblatt-Text hier einfügen …"
className="w-full text-sm rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 p-2 mt-3 mb-3" rows={5}
/>
<button onClick={run} disabled={loading || text.length < 50}
className="rounded bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white text-sm px-4 py-2">
{loading ? 'Analysiere …' : 'Grenzen extrahieren'}
</button>
{res && (
<div className="mt-5 space-y-4">
{res.llm_status === 'unavailable' && (
<div className="rounded-lg border border-amber-300 bg-amber-50 dark:bg-amber-900/20 text-amber-900 dark:text-amber-200 p-3 text-xs">
KI-Extraktion gerade nicht verfügbar (lokales Modell lädt oder offline). Unten stehen nur
deterministisch erkannte Werte bitte Grenzen extrahieren" erneut klicken oder Felder manuell ergänzen.
</div>
)}
{(res.detected.interfaces.length > 0 || res.detected.units.length > 0) && (
<div className="text-xs text-gray-600 dark:text-gray-300">
<span className="font-medium">Deterministisch erkannt:</span>{' '}
{[...res.detected.interfaces, ...res.detected.units].map((d) => (
<span key={d} className="inline-block rounded bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 text-[11px] mr-1 mb-1">{d}</span>
))}
</div>
)}
{/* Aus dem Datenblatt übernommen */}
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200">
Aus dem Datenblatt übernommen <span className="text-gray-400 font-normal">({res.filled.length})</span>
</h3>
<ul className="mt-2 space-y-1.5">
{res.filled.map((k) => (
<li key={k} className="text-xs text-gray-700 dark:text-gray-200">
<span className="font-medium">{FIELD_LABEL[k] || k}:</span> {res.limits[k]}
{res.provenance[k] && (
<span className="block text-[10px] text-gray-400 italic">Quelle: „{res.provenance[k]}"</span>
)}
</li>
))}
{res.filled.length === 0 && <li className="text-xs text-gray-400">Nichts eindeutig erkannt bitte unten ergänzen.</li>}
</ul>
</div>
{/* Rückfragen — nicht im Datenblatt */}
{res.followup.length > 0 && (
<div className="rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50/60 dark:bg-amber-900/20 p-3">
<h3 className="text-sm font-semibold text-amber-900 dark:text-amber-200">
Rückfragen steht nicht im Datenblatt ({res.followup.length})
</h3>
<p className="text-xs text-amber-800/80 dark:text-amber-300/80 mb-2">
Diese Angaben braucht die CE-Risikobeurteilung, ein Datenblatt liefert sie typischerweise nicht.
</p>
<div className="space-y-2">
{res.followup.map((f) => (
<div key={f.key}>
<label className="block text-xs text-gray-700 dark:text-gray-200">{f.question}</label>
<input value={answers[f.key] || ''} onChange={(e) => setAnswers((a) => ({ ...a, [f.key]: e.target.value }))}
className="w-full text-sm rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 p-1.5 mt-0.5"
placeholder={f.label} />
</div>
))}
</div>
</div>
)}
<div className="flex flex-wrap items-center gap-3">
<button onClick={createProject} disabled={creating}
className="rounded bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white text-sm px-4 py-2">
{creating ? 'Lege an …' : 'IACE-Projekt anlegen (Grenzen übernehmen)'}
</button>
<span className="text-xs text-gray-500">
Übernimmt Grenzen + Antworten als <span className="font-medium">editierbaren Entwurf</span> ins
IACE-Interview jedes Feld bleibt änderbar. Manuelles Anlegen weiterhin über{' '}
<a href="/sdk/iace" className="text-indigo-600 hover:underline">iACE</a>.
</span>
</div>
<p className="text-xs text-gray-400 italic">
Aus den Grenzen leitet IACE anschließend Gefährdungen und Maßnahmen ab (Entwurf Bestätigung mit Sicherheitsingenieur).
</p>
</div>
)}
</div>
)
}