feat(iace): wire crossref into tech-file, library UI, and contract tests
Three follow-ups to the 671-norm cross-reference matrix: 1. Tech-file renderer (Go): standards_applied section now gets a deterministic Markdown appendix with the DIN/ANSI/GB/JIS mappings for the project's suggested norms. Built from registry, never hallucinated by LLM. Applied both to LLM and fallback content paths. 2. Frontend NormCrossRefPanel (Next.js): expandable row in the IACE library norms tab now has a "Internationale Aequivalenzen anzeigen" button that lazy-loads /iace/norms-library/:id/crossref and renders a colour-coded table (relation + confidence). Region labels humanised (US — ANSI, China (GB), Japan (JIS), etc.). 3. Contract tests (Go): 4 new handler tests pinning the response shape of GetNormCrossRef and ListNormCrossRefs. Equivalent to an OpenAPI snapshot for these specific endpoints — ai-compliance-sdk has no full OpenAPI baseline yet (separate ticket). Tests: 6 renderer tests + 4 handler contract tests, all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface NormMapping {
|
||||
region: string
|
||||
identifier: string
|
||||
relation: string
|
||||
confidence: string
|
||||
notes?: string
|
||||
source_url?: string
|
||||
}
|
||||
|
||||
interface CrossRefResponse {
|
||||
norm_id: string
|
||||
mappings: NormMapping[]
|
||||
notes?: string
|
||||
batch_id?: string
|
||||
}
|
||||
|
||||
const RELATION_COLORS: Record<string, string> = {
|
||||
identical: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||
equivalent: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||
partial: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
|
||||
supersedes: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||
superseded_by: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400',
|
||||
}
|
||||
|
||||
const CONFIDENCE_COLORS: Record<string, string> = {
|
||||
verified: 'text-emerald-700 dark:text-emerald-300 font-semibold',
|
||||
high: 'text-blue-700 dark:text-blue-300',
|
||||
medium: 'text-amber-700 dark:text-amber-300',
|
||||
low: 'text-red-700 dark:text-red-300',
|
||||
}
|
||||
|
||||
const REGION_LABELS: Record<string, string> = {
|
||||
'EU-DIN': 'EU (DIN)',
|
||||
'INTL-ISO': 'International (ISO/IEC)',
|
||||
'US-ANSI': 'US — ANSI',
|
||||
'US-NFPA': 'US — NFPA',
|
||||
'US-UL': 'US — UL',
|
||||
'US-OSHA': 'US — OSHA',
|
||||
'US-ASME': 'US — ASME',
|
||||
'US-ASTM': 'US — ASTM',
|
||||
'US-SAE': 'US — SAE',
|
||||
'US-NIOSH': 'US — NIOSH',
|
||||
'US-FDA': 'US — FDA',
|
||||
'US-EPA': 'US — EPA',
|
||||
'US-NEMA': 'US — NEMA',
|
||||
'US-NSF': 'US — NSF',
|
||||
'US-API': 'US — API',
|
||||
'US-CPSC': 'US — CPSC',
|
||||
'US-AHRI': 'US — AHRI',
|
||||
'US-ASHRAE': 'US — ASHRAE',
|
||||
'US-FCC': 'US — FCC',
|
||||
'US-DOT': 'US — DOT',
|
||||
'CN-GB': 'China (GB)',
|
||||
'JP-JIS': 'Japan (JIS)',
|
||||
}
|
||||
|
||||
function formatRegion(region: string): string {
|
||||
return REGION_LABELS[region] || region
|
||||
}
|
||||
|
||||
interface Props {
|
||||
normId: string
|
||||
}
|
||||
|
||||
export default function NormCrossRefPanel({ normId }: Props) {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [data, setData] = useState<CrossRefResponse | null>(null)
|
||||
|
||||
const handleLoad = async () => {
|
||||
if (loaded || loading) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/iace/norms-library/${encodeURIComponent(normId)}/crossref`)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const json = (await res.json()) as CrossRefResponse
|
||||
setData(json)
|
||||
setLoaded(true)
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Fehler beim Laden')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!loaded && !loading && !error) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLoad}
|
||||
className="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200 font-medium underline-offset-2 hover:underline"
|
||||
>
|
||||
Internationale Aequivalenzen anzeigen (DIN/ANSI/GB/JIS)
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-xs text-gray-500 dark:text-gray-400">Cross-Reference wird geladen…</div>
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-xs text-red-600 dark:text-red-400">
|
||||
Cross-Reference konnte nicht geladen werden: {error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data || data.mappings.length === 0) {
|
||||
return (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 italic">
|
||||
Fuer diese Norm liegt (noch) kein internationales Mapping in der Bibliothek vor.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-2 border-t border-gray-200 dark:border-gray-700 pt-2">
|
||||
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
Internationale Aequivalenzen
|
||||
</div>
|
||||
{data.notes && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 italic">{data.notes}</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||
<th className="text-left py-1 pr-3 font-medium">Region</th>
|
||||
<th className="text-left py-1 pr-3 font-medium">Identifier</th>
|
||||
<th className="text-left py-1 pr-3 font-medium">Relation</th>
|
||||
<th className="text-left py-1 pr-3 font-medium">Confidence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.mappings.map((m, i) => (
|
||||
<tr key={i} className="border-b border-gray-100 dark:border-gray-800 last:border-0 align-top">
|
||||
<td className="py-1 pr-3 text-gray-600 dark:text-gray-400 whitespace-nowrap">{formatRegion(m.region)}</td>
|
||||
<td className="py-1 pr-3 font-mono text-gray-800 dark:text-gray-200">
|
||||
{m.source_url ? (
|
||||
<a href={m.source_url} target="_blank" rel="noopener noreferrer" className="text-purple-600 hover:text-purple-800 dark:text-purple-400">
|
||||
{m.identifier}
|
||||
</a>
|
||||
) : (
|
||||
m.identifier
|
||||
)}
|
||||
{m.notes && (
|
||||
<div className="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5 font-sans">{m.notes}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-1 pr-3">
|
||||
<span className={`inline-block px-1.5 py-0.5 rounded ${RELATION_COLORS[m.relation] || 'bg-gray-100 dark:bg-gray-800 text-gray-600'}`}>
|
||||
{m.relation}
|
||||
</span>
|
||||
</td>
|
||||
<td className={`py-1 pr-3 ${CONFIDENCE_COLORS[m.confidence] || 'text-gray-600'}`}>
|
||||
{m.confidence}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-400 dark:text-gray-500">
|
||||
Vor Nutzung in einem Drittmarkt durch eine sachkundige Person verifizieren.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useMemo, useState, useRef, useEffect } from 'react'
|
||||
import { SearchInput, FilterDropdown, Pagination, ExpandableRow, ExternalLinkIcon } from './LibraryTable'
|
||||
import NormCrossRefPanel from './NormCrossRefPanel'
|
||||
|
||||
export interface Norm {
|
||||
id: string
|
||||
@@ -128,6 +129,7 @@ export default function NormenTab({ norms }: Props) {
|
||||
{n.tags.map((t) => <span key={t} className="px-1.5 py-0.5 rounded text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300">{t}</span>)}
|
||||
</div>
|
||||
)}
|
||||
<NormCrossRefPanel normId={n.id} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user