From d80cb9c8e4f527d2ee653e8b0d7d95d2e2e0bc55 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 5 May 2026 08:22:59 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20IACE=20Interview=20Frontend=20=E2=80=94?= =?UTF-8?q?=203=20Modi=20(Interview/Wizard/Formular)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CE-Risikobeurteilung Datenerfassung mit 3 wählbaren Eingabe-Modi: 1. Interview-Modus (Chat-artig): Fragen werden nacheinander gestellt wie im Kundengespräch. Antwort-Historie sichtbar. 2. Wizard-Modus: Schritt-für-Schritt durch 8 Sektionen. 3. Formular-Modus: Alle Sektionen als Accordion auf einer Seite. 20 strukturierte Fragen in 8 Abschnitten: - Maschinenbeschreibung (Name, Typ, Baugruppen) - Lebensphasen (Betrieb, Einrichten, Wartung) - Bestimmungsgemäße Verwendung - Vorhersehbare Fehlanwendung - Qualifikation der Benutzer - Räumliche/Zeitliche Grenzen - Technische Daten (Kräfte, Spannungen, Temperaturen, Drehzahlen) - Umgebungsbedingungen answersToNarrativeText() konvertiert alle Antworten in den Freitext der an POST /parse-narrative gesendet wird. Ergebnis-Panel zeigt: Komponenten, Gefahren, Patterns, Energiequellen. URL: /sdk/iace/[projectId]/interview Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sdk/iace/[projectId]/interview/_types.ts | 85 ++++++ .../sdk/iace/[projectId]/interview/page.tsx | 285 ++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/interview/_types.ts create mode 100644 admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx diff --git a/admin-compliance/app/sdk/iace/[projectId]/interview/_types.ts b/admin-compliance/app/sdk/iace/[projectId]/interview/_types.ts new file mode 100644 index 0000000..1086b2d --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/interview/_types.ts @@ -0,0 +1,85 @@ +// IACE Interview Types — structured questions based on CE risk assessment document structure + +export interface InterviewQuestion { + id: string + section: number + sectionTitle: string + question: string + type: 'text' | 'textarea' | 'select' | 'multiselect' | 'number' + options?: string[] + placeholder?: string + helpText?: string + required?: boolean +} + +export interface InterviewAnswer { + questionId: string + value: string | string[] | number +} + +export const INTERVIEW_QUESTIONS: InterviewQuestion[] = [ + // Section 1: Maschinenbeschreibung + { id: 'machine_name', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Wie heisst die Maschine / Anlage?', type: 'text', placeholder: 'z.B. Kniehebelpresse HP-500', required: true }, + { id: 'machine_type', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Welcher Maschinentyp ist es?', type: 'select', options: ['Presse', 'Roboter', 'CNC-Maschine', 'Foerderanlage', 'Verpackungsmaschine', 'Schweissanlage', 'Montageanlage', 'Sondermaschine'], required: true }, + { id: 'manufacturer', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Wer ist der Hersteller?', type: 'text', placeholder: 'z.B. Mueller Maschinenbau GmbH' }, + { id: 'description', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Beschreiben Sie die Anlage und ihre Funktion:', type: 'textarea', placeholder: 'Die Anlage ist eine vollautomatische...', helpText: 'Beschreiben Sie den Zweck, die Arbeitsweise und den Aufbau der Maschine.', required: true }, + { id: 'components', section: 1, sectionTitle: 'Maschinenbeschreibung', question: 'Aus welchen Baugruppen besteht die Anlage?', type: 'multiselect', options: ['Zufuehrung', 'Presse/Umformung', 'Transferanlage', 'Foerderband', 'Roboter', 'Absaugung', 'Schmieranlage', 'Schutzumhausung', 'Aufzug/Hubwerk', 'Schaltschrank/Steuerung', 'Kuehlung', 'Heizung', 'Hydraulik', 'Pneumatik'] }, + + // Section 2: Lebensphasen + { id: 'lifecycle_operation', section: 2, sectionTitle: 'Lebensphasen', question: 'Wie laeuft der Normalbetrieb ab?', type: 'textarea', placeholder: 'Die Bearbeitung erfolgt vollautomatisch...', helpText: 'Beschreiben Sie den typischen Produktionszyklus.' }, + { id: 'lifecycle_setup', section: 2, sectionTitle: 'Lebensphasen', question: 'Welche Arbeiten fallen beim Einrichten/Umruesten an?', type: 'textarea', placeholder: 'Werkzeugwechsel, Parameteranpassung...' }, + { id: 'lifecycle_maintenance', section: 2, sectionTitle: 'Lebensphasen', question: 'Welche Wartungs- und Reinigungsarbeiten sind noetig?', type: 'textarea', placeholder: 'Woechentliche Schmierung, Filter reinigen...' }, + + // Section 3: Bestimmungsgemäße Verwendung + { id: 'intended_use', section: 3, sectionTitle: 'Bestimmungsgemäße Verwendung', question: 'Wozu dient die Maschine (bestimmungsgemäße Verwendung)?', type: 'textarea', placeholder: 'Die Anlage dient der automatischen...', required: true }, + + // Section 4: Vorhersehbare Fehlanwendung + { id: 'misuse', section: 4, sectionTitle: 'Vorhersehbare Fehlanwendung', question: 'Welche vorhersehbaren Fehlanwendungen sind moeglich?', type: 'multiselect', options: ['Ueberschreiten von Belastungsgrenzen', 'Verwendung ungeeigneter Materialien', 'Betrieb in explosionsgefaehrdeter Atmosphaere', 'Betrieb bei Leckagen', 'Betrieb ohne PSA', 'Umgehung von Sicherheitseinrichtungen', 'Bedienung ohne Einweisung', 'Manipulation der Steuerung'], helpText: 'Waehlen Sie alle zutreffenden oder ergaenzen Sie.' }, + + // Section 5: Qualifikation + { id: 'operator_qualification', section: 5, sectionTitle: 'Qualifikation der Benutzer', question: 'Welche Qualifikation hat das Bedienpersonal?', type: 'select', options: ['Eingewiesenes Personal ohne Fachkenntnisse', 'Angelernte Mitarbeiter', 'Facharbeiter mit Berufsausbildung', 'Ingenieure/Techniker', 'Elektrofachkraefte'] }, + { id: 'maintenance_qualification', section: 5, sectionTitle: 'Qualifikation der Benutzer', question: 'Wer fuehrt Wartung/Instandhaltung durch?', type: 'select', options: ['Eigenes Fachpersonal', 'Hersteller-Service', 'Fremdfirma', 'Nicht separat betrachtet (CE-Erklaerung Lieferant)'] }, + + // Section 6: Grenzen + { id: 'spatial_limits', section: 6, sectionTitle: 'Raeumliche und zeitliche Grenzen', question: 'Welche Gefahrenbereiche gibt es?', type: 'textarea', placeholder: 'Werkzeugeinbauraum, Zufuehrbereich, Auslaufbereich...', helpText: 'Listen Sie alle Bereiche auf, in denen Personen gefaehrdet sein koennten.' }, + { id: 'safety_measures_org', section: 6, sectionTitle: 'Raeumliche und zeitliche Grenzen', question: 'Welche organisatorischen Schutzmassnahmen gelten?', type: 'multiselect', options: ['Sicherheitsschuhe Pflicht', 'Gehoerschutz Pflicht', 'Handschuhe Pflicht', 'Schutzbrille Pflicht', 'Zutrittsbeschraenkung', 'Unterweisung vor Zugang'] }, + + // Section 7: Technische Daten + { id: 'force_pressure', section: 7, sectionTitle: 'Technische Daten', question: 'Welche Kraefte/Druecke wirken? (kN, bar, Tonnen)', type: 'text', placeholder: 'z.B. 20000 kN, 250 bar' }, + { id: 'voltage', section: 7, sectionTitle: 'Technische Daten', question: 'Welche Spannungen sind vorhanden? (V)', type: 'text', placeholder: 'z.B. 400V Hauptstrom, 24V Steuerung' }, + { id: 'temperature', section: 7, sectionTitle: 'Technische Daten', question: 'Treten erhoehte Temperaturen auf? (°C)', type: 'text', placeholder: 'z.B. 130°C Werkstuecktemperatur' }, + { id: 'speed_rpm', section: 7, sectionTitle: 'Technische Daten', question: 'Welche Geschwindigkeiten/Drehzahlen gibt es? (/min, m/s)', type: 'text', placeholder: 'z.B. 736 /min Schwungrad, 36 Huebe/min' }, + { id: 'energy', section: 7, sectionTitle: 'Technische Daten', question: 'Welches Arbeitsvermoegen hat die Maschine? (kJ, kW)', type: 'text', placeholder: 'z.B. 400 kJ, 3 kW Motor' }, + + // Section 8: Umgebung + { id: 'environment', section: 8, sectionTitle: 'Umgebungsbedingungen', question: 'Unter welchen Umgebungsbedingungen wird die Maschine betrieben?', type: 'textarea', placeholder: '+5 bis +40°C, max. 95% Luftfeuchte, bis 1000m ueNN', helpText: 'Temperatur, Luftfeuchte, Hoehenlage, besondere Bedingungen.' }, +] + +export function answersToNarrativeText(answers: InterviewAnswer[]): string { + const parts: string[] = [] + const getVal = (id: string) => { + const a = answers.find(a => a.questionId === id) + if (!a) return '' + return Array.isArray(a.value) ? (a.value as string[]).join(', ') : String(a.value) + } + + parts.push(`Maschinenname: ${getVal('machine_name')}. Maschinentyp: ${getVal('machine_type')}. Hersteller: ${getVal('manufacturer')}.`) + if (getVal('description')) parts.push(getVal('description')) + if (getVal('components')) parts.push(`Baugruppen: ${getVal('components')}.`) + if (getVal('lifecycle_operation')) parts.push(`Betrieb: ${getVal('lifecycle_operation')}`) + if (getVal('lifecycle_setup')) parts.push(`Einrichten: ${getVal('lifecycle_setup')}`) + if (getVal('lifecycle_maintenance')) parts.push(`Wartung: ${getVal('lifecycle_maintenance')}`) + if (getVal('intended_use')) parts.push(`Bestimmungsgemäße Verwendung: ${getVal('intended_use')}`) + if (getVal('misuse')) parts.push(`Vorhersehbare Fehlanwendung: ${getVal('misuse')}`) + if (getVal('operator_qualification')) parts.push(`Bedienpersonal: ${getVal('operator_qualification')}`) + if (getVal('spatial_limits')) parts.push(`Gefahrenbereiche: ${getVal('spatial_limits')}`) + if (getVal('safety_measures_org')) parts.push(`Organisatorische Massnahmen: ${getVal('safety_measures_org')}`) + if (getVal('force_pressure')) parts.push(getVal('force_pressure')) + if (getVal('voltage')) parts.push(getVal('voltage')) + if (getVal('temperature')) parts.push(getVal('temperature')) + if (getVal('speed_rpm')) parts.push(getVal('speed_rpm')) + if (getVal('energy')) parts.push(getVal('energy')) + if (getVal('environment')) parts.push(`Umgebung: ${getVal('environment')}`) + + return parts.join('\n') +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx new file mode 100644 index 0000000..7d605f9 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx @@ -0,0 +1,285 @@ +'use client' + +import { useState } from 'react' +import { useParams } from 'next/navigation' +import { INTERVIEW_QUESTIONS, answersToNarrativeText, type InterviewAnswer, type InterviewQuestion } from './_types' + +type InputMode = 'interview' | 'wizard' | 'form' + +export default function IACEInterviewPage() { + const { projectId } = useParams<{ projectId: string }>() + const [mode, setMode] = useState('interview') + const [answers, setAnswers] = useState([]) + const [currentQ, setCurrentQ] = useState(0) + const [currentSection, setCurrentSection] = useState(1) + const [analyzing, setAnalyzing] = useState(false) + const [result, setResult] = useState(null) + const [inputValue, setInputValue] = useState('') + const [multiValue, setMultiValue] = useState([]) + + const setAnswer = (qId: string, value: string | string[] | number) => { + setAnswers(prev => { + const existing = prev.findIndex(a => a.questionId === qId) + if (existing >= 0) { prev[existing].value = value; return [...prev] } + return [...prev, { questionId: qId, value }] + }) + } + + const getAnswer = (qId: string) => answers.find(a => a.questionId === qId)?.value || '' + + const handleAnalyze = async () => { + setAnalyzing(true) + const narrativeText = answersToNarrativeText(answers) + try { + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/parse-narrative`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ narrative_text: narrativeText }), + }) + if (res.ok) setResult(await res.json()) + } catch { /* ignore */ } + setAnalyzing(false) + } + + const q = INTERVIEW_QUESTIONS[currentQ] + const sections = [...new Set(INTERVIEW_QUESTIONS.map(q => q.section))] + const sectionQuestions = (s: number) => INTERVIEW_QUESTIONS.filter(q => q.section === s) + + // Interview mode: advance to next question + const handleInterviewNext = () => { + if (q.type === 'multiselect') { setAnswer(q.id, multiValue); setMultiValue([]) } + else if (inputValue) { setAnswer(q.id, inputValue); setInputValue('') } + if (currentQ < INTERVIEW_QUESTIONS.length - 1) setCurrentQ(currentQ + 1) + } + + return ( +
+ {/* Mode Switcher */} +
+

CE-Risikobeurteilung — Datenerfassung

+
+ {([['interview', 'Interview'], ['wizard', 'Wizard'], ['form', 'Formular']] as [InputMode, string][]).map(([m, label]) => ( + + ))} +
+
+ + {/* Result */} + {result && ( +
+

Analyse-Ergebnis (deterministisch)

+
+
{result.components?.length || 0}
Komponenten
+
{result.suggested_hazards?.length || 0}
Gefahren
+
{result.matched_patterns || 0}
Patterns
+
{result.energy_sources?.length || 0}
Energiequellen
+
+ {result.suggested_hazards?.length > 0 && ( +
+

Erkannte Gefahren:

+ {result.suggested_hazards.map((h: any, i: number) => ( +
+ = 90 ? 'bg-red-100 text-red-700' : h.priority >= 70 ? 'bg-orange-100 text-orange-700' : 'bg-yellow-100 text-yellow-700'}`}>P{h.priority} + {h.pattern_name || h.category} + {h.category} +
+ ))} +
+ )} +
+ )} + + {/* ═══════════════ INTERVIEW MODE ═══════════════ */} + {mode === 'interview' && !result && ( +
+
+ {/* Previous answers (chat history) */} +
+ {INTERVIEW_QUESTIONS.slice(0, currentQ).map((pq, i) => { + const ans = getAnswer(pq.id) + if (!ans || (Array.isArray(ans) && ans.length === 0)) return null + return ( +
+
{pq.question}
+
+ {Array.isArray(ans) ? ans.join(', ') : String(ans)} +
+
+ ) + })} +
+ + {/* Current question */} + {currentQ < INTERVIEW_QUESTIONS.length && ( +
+
Frage {currentQ + 1}/{INTERVIEW_QUESTIONS.length} — {q.sectionTitle}
+
{q.question}
+ {q.helpText &&

{q.helpText}

} + + {q.type === 'text' && ( + setInputValue(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleInterviewNext()} + placeholder={q.placeholder} className="w-full px-4 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500" autoFocus /> + )} + {q.type === 'textarea' && ( +