feat(cra): Datenblatt-Analyse-Frontend (Grenzen-Extraktion + Rückfragen)

DatasheetExtract auf /sdk/cra: Datenblatt einfügen (oder Beispiel OWIS/Zwick) →
POST /extract-datasheet → gefuellte ISO-12100-Grenzen mit Quellen-Zitat +
deterministisch erkannte Schnittstellen/Einheiten + gezielte Rückfragen fuer
leere Pflichtfelder (foreseeable_misuses, person_groups, …). Vorstufe fuer
'Projekt anlegen' → IACE-Grenzen-Prefill.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-16 19:20:14 +02:00
parent cfdc5fe277
commit 6ca085ffc5
3 changed files with 163 additions and 0 deletions
@@ -0,0 +1,133 @@
'use client'
import { useState } from 'react'
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[] }
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 [text, setText] = useState('')
const [res, setRes] = useState<ExtractResult | null>(null)
const [loading, setLoading] = 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) }
}
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.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>
)}
<p className="text-xs text-gray-500 italic">
Nächster Schritt: „Projekt anlegen" überträgt diese Grenzen + Antworten in das IACE-Modul; daraus
werden Gefährdungen und Maßnahmen abgeleitet (Entwurf Bestätigung mit Sicherheitsingenieur).
</p>
</div>
)}
</div>
)
}
@@ -58,3 +58,30 @@ export const READINESS_PRESETS: ReadinessPreset[] = [
hazard_types: ['movement_crush'], hazard_types: ['movement_crush'],
}, },
] ]
// Example datasheet texts for the extraction demo (paste-equivalent). Sourced
// from public datasheets (owis.eu PS 90+, zwickroell.com roboTest / testXpert).
export const DATASHEET_EXAMPLES: { id: string; label: string; text: string }[] = [
{
id: 'owis',
label: 'OWIS PS 90+',
text:
'OWIS PS 90+ — Universelle Positioniersteuerung. Bis zu 9 Achsen für Schritt-, DC-, ' +
'BLDC- und Linearmotoren. Schnittstellen: USB, RS232, Ethernet; optionales Anybus-Modul ' +
'(Modbus/TCP) für industrielle Netzwerke. 8 TTL-, 8 Analog- und 8 SPS-Ein-/Ausgänge. ' +
'4 konfigurierbare Endschalter-Eingänge pro Achse. Ereignisbasierte Triggersignale. ' +
'Betriebssoftware OWISoft 3.0. SDK für C, C++, C#, LabView (32/64 Bit). Versorgung 24 V. ' +
'Optionale Motor-Haltebremsen für bis zu 4 Achsen. Anschluss für externen Not-Halt-Taster. ' +
'PC-Betrieb unter Windows 10/11.',
},
{
id: 'zwick',
label: 'ZwickRoell roboTest',
text:
'ZwickRoell roboTest — automatisiertes Prüfsystem. Roboter-Probenhandling für 24/7-Betrieb. ' +
'Software autoEdition (Datenmanagement, Steuerung, Prozessvisualisierung) und Prüfsoftware ' +
'testXpert (Windows 10/11). Volle Prozesssteuerung per Browser; Systemstatus auf Tablet-PCs. ' +
'Direkte Datenanbindung an Host-Systeme (ERP/LIMS/QA). Umfangreiche Benutzer- und ' +
'Rechteverwaltung. 2D-Barcode/QR-Scanner. Automatische Querschnittsmessung mit Tastern.',
},
]
+3
View File
@@ -4,6 +4,7 @@ import React, { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { ClassificationBadge } from './_components/ClassificationBadge' import { ClassificationBadge } from './_components/ClassificationBadge'
import { ReadinessCheck } from './_components/ReadinessCheck' import { ReadinessCheck } from './_components/ReadinessCheck'
import { DatasheetExtract } from './_components/DatasheetExtract'
interface CRAProject { interface CRAProject {
id: string id: string
@@ -102,6 +103,8 @@ export default function CRAProjectsPage() {
<ReadinessCheck onCreateProject={() => setShowModal(true)} /> <ReadinessCheck onCreateProject={() => setShowModal(true)} />
<DatasheetExtract />
{/* Bridge: vom Readiness-Check in die kombinierte CE × Cyber-Analyse */} {/* Bridge: vom Readiness-Check in die kombinierte CE × Cyber-Analyse */}
<a <a
href="/sdk/iace/e79921be-c78a-47ab-8dfa-5fa48ba8a34a/cra" href="/sdk/iace/e79921be-c78a-47ab-8dfa-5fa48ba8a34a/cra"