feat(iace): Phase 5+6 — frontend integration, RAG library search, comprehensive tests
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
Phase 5 — Frontend Integration: - components/page.tsx: ComponentLibraryModal with 120 components + 20 energy sources - hazards/page.tsx: AutoSuggestPanel with 3-column pattern matching review - mitigations/page.tsx: SuggestMeasuresModal per hazard with 3-level grouping - verification/page.tsx: SuggestEvidenceModal per mitigation with evidence types Phase 6 — RAG Library Search: - Added bp_iace_libraries to AllowedCollections whitelist in rag_handlers.go - SearchLibrary endpoint: POST /iace/library-search (semantic search across libraries) - EnrichTechFileSection endpoint: POST /projects/:id/tech-file/:section/enrich - Created ingest-iace-libraries.sh ingestion script for Qdrant collection Tests (123 passing): - tag_taxonomy_test.go: 8 tests for taxonomy entries, domains, essential tags - controls_library_test.go: 7 tests for measures, reduction types, subtypes - integration_test.go: 7 integration tests for full match flow and library consistency - Extended tag_resolver_test.go: 9 new tests for FindByTags and cross-category resolution Documentation: - Updated iace.md with Hazard-Matching-Engine, RAG enrichment, and new DB tables Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ interface Hazard {
|
||||
hazardous_zone: string
|
||||
review_status: string
|
||||
created_at: string
|
||||
source?: string
|
||||
}
|
||||
|
||||
interface LibraryHazard {
|
||||
@@ -60,6 +61,38 @@ interface RoleInfo {
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
// Pattern matching types (Phase 5)
|
||||
interface PatternMatch {
|
||||
pattern_id: string
|
||||
pattern_name: string
|
||||
priority: number
|
||||
matched_tags: string[]
|
||||
}
|
||||
|
||||
interface HazardSuggestion {
|
||||
category: string
|
||||
source_patterns: string[]
|
||||
confidence: number
|
||||
}
|
||||
|
||||
interface MeasureSuggestion {
|
||||
measure_id: string
|
||||
source_patterns: string[]
|
||||
}
|
||||
|
||||
interface EvidenceSuggestion {
|
||||
evidence_id: string
|
||||
source_patterns: string[]
|
||||
}
|
||||
|
||||
interface MatchOutput {
|
||||
matched_patterns: PatternMatch[]
|
||||
suggested_hazards: HazardSuggestion[]
|
||||
suggested_measures: MeasureSuggestion[]
|
||||
suggested_evidence: EvidenceSuggestion[]
|
||||
resolved_tags: string[]
|
||||
}
|
||||
|
||||
// ISO 12100 Hazard Categories (A-J)
|
||||
const HAZARD_CATEGORIES = [
|
||||
'mechanical', 'electrical', 'thermal',
|
||||
@@ -128,7 +161,7 @@ function getRiskColor(level: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// ISO 12100 mode risk levels (S×F×P×A, max 625)
|
||||
// ISO 12100 mode risk levels (S*F*P*A, max 625)
|
||||
function getRiskLevelISO(r: number): string {
|
||||
if (r > 300) return 'not_acceptable'
|
||||
if (r >= 151) return 'very_high'
|
||||
@@ -137,7 +170,7 @@ function getRiskLevelISO(r: number): string {
|
||||
return 'low'
|
||||
}
|
||||
|
||||
// Legacy mode (S×E×P, max 125)
|
||||
// Legacy mode (S*E*P, max 125)
|
||||
function getRiskLevelLegacy(r: number): string {
|
||||
if (r >= 100) return 'critical'
|
||||
if (r >= 50) return 'high'
|
||||
@@ -227,7 +260,7 @@ function HazardForm({
|
||||
|
||||
const [showExtended, setShowExtended] = useState(false)
|
||||
|
||||
// ISO 12100 mode: S × F × P × A when avoidance is set
|
||||
// ISO 12100 mode: S * F * P * A when avoidance is set
|
||||
const isISOMode = formData.avoidance > 0
|
||||
const rInherent = isISOMode
|
||||
? formData.severity * formData.exposure * formData.probability * formData.avoidance
|
||||
@@ -572,6 +605,232 @@ function LibraryModal({
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Auto-Suggest Panel (Phase 5 — Pattern Matching)
|
||||
// ============================================================================
|
||||
|
||||
function AutoSuggestPanel({
|
||||
projectId,
|
||||
matchResult,
|
||||
applying,
|
||||
onApply,
|
||||
onClose,
|
||||
}: {
|
||||
projectId: string
|
||||
matchResult: MatchOutput
|
||||
applying: boolean
|
||||
onApply: (acceptedHazardCats: string[], acceptedMeasureIds: string[], acceptedEvidenceIds: string[], patternIds: string[]) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [selectedHazards, setSelectedHazards] = useState<Set<string>>(
|
||||
new Set(matchResult.suggested_hazards.map(h => h.category))
|
||||
)
|
||||
const [selectedMeasures, setSelectedMeasures] = useState<Set<string>>(
|
||||
new Set(matchResult.suggested_measures.map(m => m.measure_id))
|
||||
)
|
||||
const [selectedEvidence, setSelectedEvidence] = useState<Set<string>>(
|
||||
new Set(matchResult.suggested_evidence.map(e => e.evidence_id))
|
||||
)
|
||||
|
||||
function toggleHazard(cat: string) {
|
||||
setSelectedHazards(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(cat)) next.delete(cat)
|
||||
else next.add(cat)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function toggleMeasure(id: string) {
|
||||
setSelectedMeasures(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function toggleEvidence(id: string) {
|
||||
setSelectedEvidence(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const totalSelected = selectedHazards.size + selectedMeasures.size + selectedEvidence.size
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border-2 border-purple-300 p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Pattern-Matching Ergebnisse
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{matchResult.matched_patterns.length} Patterns erkannt, {matchResult.resolved_tags.length} Tags aufgeloest
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Matched Patterns */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
Erkannte Patterns ({matchResult.matched_patterns.length})
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{matchResult.matched_patterns
|
||||
.sort((a, b) => b.priority - a.priority)
|
||||
.map(p => (
|
||||
<span key={p.pattern_id} className="inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs bg-purple-50 text-purple-700 border border-purple-200">
|
||||
<span className="font-mono">{p.pattern_id}</span>
|
||||
<span>{p.pattern_name}</span>
|
||||
<span className="text-purple-400">P:{p.priority}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3-Column Review */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
{/* Hazards */}
|
||||
<div className="border border-orange-200 rounded-lg p-3 bg-orange-50/50">
|
||||
<h4 className="text-sm font-semibold text-orange-800 mb-2">
|
||||
Gefaehrdungen ({matchResult.suggested_hazards.length})
|
||||
</h4>
|
||||
<div className="space-y-2 max-h-48 overflow-auto">
|
||||
{matchResult.suggested_hazards.map(h => (
|
||||
<label key={h.category} className="flex items-start gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedHazards.has(h.category)}
|
||||
onChange={() => toggleHazard(h.category)}
|
||||
className="mt-0.5 accent-purple-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{CATEGORY_LABELS[h.category] || h.category}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Konfidenz: {Math.round(h.confidence * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Measures */}
|
||||
<div className="border border-green-200 rounded-lg p-3 bg-green-50/50">
|
||||
<h4 className="text-sm font-semibold text-green-800 mb-2">
|
||||
Massnahmen ({matchResult.suggested_measures.length})
|
||||
</h4>
|
||||
<div className="space-y-2 max-h-48 overflow-auto">
|
||||
{matchResult.suggested_measures.map(m => (
|
||||
<label key={m.measure_id} className="flex items-start gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMeasures.has(m.measure_id)}
|
||||
onChange={() => toggleMeasure(m.measure_id)}
|
||||
className="mt-0.5 accent-purple-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-900 font-mono">{m.measure_id}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
von {m.source_patterns.length} Pattern(s)
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Evidence */}
|
||||
<div className="border border-blue-200 rounded-lg p-3 bg-blue-50/50">
|
||||
<h4 className="text-sm font-semibold text-blue-800 mb-2">
|
||||
Nachweise ({matchResult.suggested_evidence.length})
|
||||
</h4>
|
||||
<div className="space-y-2 max-h-48 overflow-auto">
|
||||
{matchResult.suggested_evidence.map(e => (
|
||||
<label key={e.evidence_id} className="flex items-start gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedEvidence.has(e.evidence_id)}
|
||||
onChange={() => toggleEvidence(e.evidence_id)}
|
||||
className="mt-0.5 accent-purple-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-900 font-mono">{e.evidence_id}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
von {e.source_patterns.length} Pattern(s)
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{matchResult.resolved_tags.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-gray-500 mb-1">Aufgeloeste Tags</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{matchResult.resolved_tags.map(tag => (
|
||||
<span key={tag} className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-500">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-200">
|
||||
<span className="text-sm text-gray-500">
|
||||
{totalSelected} Elemente ausgewaehlt
|
||||
</span>
|
||||
<div className="flex gap-3">
|
||||
<button onClick={onClose} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onApply(
|
||||
Array.from(selectedHazards),
|
||||
Array.from(selectedMeasures),
|
||||
Array.from(selectedEvidence),
|
||||
matchResult.matched_patterns.map(p => p.pattern_id),
|
||||
)}
|
||||
disabled={totalSelected === 0 || applying}
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
|
||||
totalSelected > 0 && !applying
|
||||
? 'bg-purple-600 text-white hover:bg-purple-700'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{applying ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
Wird uebernommen...
|
||||
</span>
|
||||
) : (
|
||||
`${totalSelected} uebernehmen`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Page
|
||||
// ============================================================================
|
||||
|
||||
export default function HazardsPage() {
|
||||
const params = useParams()
|
||||
const projectId = params.projectId as string
|
||||
@@ -583,6 +842,10 @@ export default function HazardsPage() {
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [showLibrary, setShowLibrary] = useState(false)
|
||||
const [suggestingAI, setSuggestingAI] = useState(false)
|
||||
// Pattern matching state (Phase 5)
|
||||
const [matchingPatterns, setMatchingPatterns] = useState(false)
|
||||
const [matchResult, setMatchResult] = useState<MatchOutput | null>(null)
|
||||
const [applyingPatterns, setApplyingPatterns] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchHazards()
|
||||
@@ -698,6 +961,99 @@ export default function HazardsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern matching (Phase 5)
|
||||
async function handlePatternMatching() {
|
||||
setMatchingPatterns(true)
|
||||
setMatchResult(null)
|
||||
try {
|
||||
// First, fetch component library IDs and energy source IDs from project components
|
||||
const compRes = await fetch(`/api/sdk/v1/iace/projects/${projectId}/components`)
|
||||
let componentLibraryIds: string[] = []
|
||||
let energySourceIds: string[] = []
|
||||
if (compRes.ok) {
|
||||
const compJson = await compRes.json()
|
||||
const comps = compJson.components || compJson || []
|
||||
componentLibraryIds = comps
|
||||
.map((c: { library_component_id?: string }) => c.library_component_id)
|
||||
.filter(Boolean) as string[]
|
||||
const allEnergyIds = comps
|
||||
.flatMap((c: { energy_source_ids?: string[] }) => c.energy_source_ids || [])
|
||||
energySourceIds = [...new Set(allEnergyIds)] as string[]
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/match-patterns`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
component_library_ids: componentLibraryIds,
|
||||
energy_source_ids: energySourceIds,
|
||||
lifecycle_phases: [],
|
||||
custom_tags: [],
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
const result: MatchOutput = await res.json()
|
||||
setMatchResult(result)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to match patterns:', err)
|
||||
} finally {
|
||||
setMatchingPatterns(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApplyPatterns(
|
||||
acceptedHazardCats: string[],
|
||||
acceptedMeasureIds: string[],
|
||||
acceptedEvidenceIds: string[],
|
||||
patternIds: string[],
|
||||
) {
|
||||
setApplyingPatterns(true)
|
||||
try {
|
||||
// Build the accepted hazard requests from categories
|
||||
const acceptedHazards = acceptedHazardCats.map(cat => ({
|
||||
name: `Auto: ${CATEGORY_LABELS[cat] || cat}`,
|
||||
description: `Automatisch erkannte Gefaehrdung aus Pattern-Matching (Kategorie: ${cat})`,
|
||||
category: cat,
|
||||
severity: 3,
|
||||
exposure: 3,
|
||||
probability: 3,
|
||||
avoidance: 3,
|
||||
}))
|
||||
|
||||
const acceptedMeasures = acceptedMeasureIds.map(id => ({
|
||||
name: `Auto: Massnahme ${id}`,
|
||||
description: `Automatisch vorgeschlagene Massnahme aus Pattern-Matching`,
|
||||
reduction_type: 'design',
|
||||
}))
|
||||
|
||||
const acceptedEvidence = acceptedEvidenceIds.map(id => ({
|
||||
title: `Auto: Nachweis ${id}`,
|
||||
description: `Automatisch vorgeschlagener Nachweis aus Pattern-Matching`,
|
||||
method: 'test_report',
|
||||
}))
|
||||
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/apply-patterns`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
accepted_hazards: acceptedHazards,
|
||||
accepted_measures: acceptedMeasures,
|
||||
accepted_evidence: acceptedEvidence,
|
||||
source_pattern_ids: patternIds,
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
setMatchResult(null)
|
||||
await fetchHazards()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to apply patterns:', err)
|
||||
} finally {
|
||||
setApplyingPatterns(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm('Gefaehrdung wirklich loeschen?')) return
|
||||
try {
|
||||
@@ -729,6 +1085,20 @@ export default function HazardsPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handlePatternMatching}
|
||||
disabled={matchingPatterns}
|
||||
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors disabled:opacity-50 text-sm"
|
||||
>
|
||||
{matchingPatterns ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600" />
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
</svg>
|
||||
)}
|
||||
Auto-Erkennung
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAISuggestions}
|
||||
disabled={suggestingAI}
|
||||
@@ -764,6 +1134,35 @@ export default function HazardsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pattern Match Results (Phase 5) */}
|
||||
{matchResult && matchResult.matched_patterns.length > 0 && (
|
||||
<AutoSuggestPanel
|
||||
projectId={projectId}
|
||||
matchResult={matchResult}
|
||||
applying={applyingPatterns}
|
||||
onApply={handleApplyPatterns}
|
||||
onClose={() => setMatchResult(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* No patterns matched info */}
|
||||
{matchResult && matchResult.matched_patterns.length === 0 && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-4 flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-yellow-600 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm text-yellow-800">
|
||||
Keine Patterns erkannt. Fuegen Sie zuerst Komponenten aus der Bibliothek hinzu,
|
||||
damit die automatische Erkennung Gefaehrdungen ableiten kann.
|
||||
</p>
|
||||
<button onClick={() => setMatchResult(null)} className="text-xs text-yellow-600 hover:text-yellow-700 mt-1">
|
||||
Schliessen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
{hazards.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-7 gap-3">
|
||||
@@ -843,7 +1242,14 @@ export default function HazardsPage() {
|
||||
.map((hazard) => (
|
||||
<tr key={hazard.id} className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">{hazard.name}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">{hazard.name}</div>
|
||||
{hazard.name.startsWith('Auto:') && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">
|
||||
Auto
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hazard.description && (
|
||||
<div className="text-xs text-gray-500 truncate max-w-[250px]">{hazard.description}</div>
|
||||
)}
|
||||
@@ -890,10 +1296,17 @@ export default function HazardsPage() {
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Kein Hazard Log vorhanden</h3>
|
||||
<p className="mt-2 text-gray-500 max-w-md mx-auto">
|
||||
Beginnen Sie mit der systematischen Erfassung von Gefaehrdungen. Nutzen Sie die Bibliothek
|
||||
oder KI-Vorschlaege als Ausgangspunkt.
|
||||
Beginnen Sie mit der systematischen Erfassung von Gefaehrdungen. Nutzen Sie die Bibliothek,
|
||||
KI-Vorschlaege oder die automatische Erkennung als Ausgangspunkt.
|
||||
</p>
|
||||
<div className="mt-6 flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={handlePatternMatching}
|
||||
disabled={matchingPatterns}
|
||||
className="px-6 py-3 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{matchingPatterns ? 'Erkennung laeuft...' : 'Auto-Erkennung starten'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user