Files
breakpilot-compliance/admin-compliance/app/sdk/cra/_components/DatasheetExtract.tsx
T
Benjamin Admin b217429d39 feat(cra): Datenblatt-Extraktion auf lokales 35B + llm_status-Fix
llm_cascade additiv modell-faehig (optionaler model-Param, Cache-Key kennt
model_hint → keine Kollision; Default unveraendert für alle anderen Nutzer).
Datenblatt-Extraktor nutzt jetzt qwen3.5:35b-a3b (CRA_DATASHEET_MODEL, gleiches
Modell wie der Compliance Advisor) für bessere semantische Zuordnung. Plus
llm_status (ok|empty|unavailable) + Logging statt stillem except; Frontend zeigt
bei 'unavailable' einen Hinweis statt leerer Felder (wichtig auf prod ohne
lokales Ollama → Cascade-Fallback bzw. Hinweis).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 19:53:48 +02:00

141 lines
7.0 KiB
TypeScript

'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[] }
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 [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.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>
)}
<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>
)
}