72117c447f
CreateProjectRequest verlangt machine_name, machine_type UND manufacturer (alle required) → leere Werte gaben 400. Fallback 'Nicht angegeben', wenn das Datenblatt sie nicht liefert (im Interview editierbar). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
187 lines
9.2 KiB
TypeScript
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.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>
|
|
)
|
|
}
|