'use client'
import { useState, useEffect } from 'react'
import {
ArrowLeft, CheckCircle2, Trash2, Pencil, SkipForward,
ChevronLeft, Scale, BookOpen, ExternalLink, AlertTriangle,
FileText, Clock,
} from 'lucide-react'
import {
CanonicalControl, BACKEND_URL,
SeverityBadge, StateBadge, LicenseRuleBadge, CategoryBadge, TargetAudienceBadge,
} from './helpers'
// =============================================================================
// Compact Control Panel (used on both sides of the comparison)
// =============================================================================
function ControlPanel({ ctrl, label, highlight }: { ctrl: CanonicalControl; label: string; highlight?: boolean }) {
return (
{/* Panel Header */}
{label}
{ctrl.control_id}
{ctrl.title}
{/* Panel Content */}
{/* Objective */}
{/* Rationale */}
{ctrl.rationale && (
Begruendung
{ctrl.rationale}
)}
{/* Source Citation (Rule 1+2) */}
{ctrl.source_citation && (
Gesetzliche Grundlage
{ctrl.source_citation.source && (
{ctrl.source_citation.source}
{ctrl.source_citation.article && ` — ${ctrl.source_citation.article}`}
{ctrl.source_citation.paragraph && ` ${ctrl.source_citation.paragraph}`}
)}
)}
{/* Requirements */}
{ctrl.requirements.length > 0 && (
Anforderungen
{ctrl.requirements.map((r, i) => (
- {r}
))}
)}
{/* Test Procedure */}
{ctrl.test_procedure.length > 0 && (
Pruefverfahren
{ctrl.test_procedure.map((s, i) => (
- {s}
))}
)}
{/* Open Anchors */}
{ctrl.open_anchors.length > 0 && (
Referenzen ({ctrl.open_anchors.length})
{ctrl.open_anchors.map((a, i) => (
{a.framework}
{a.ref}
))}
)}
{/* Tags */}
{ctrl.tags.length > 0 && (
{ctrl.tags.map(t => (
{t}
))}
)}
)
}
// =============================================================================
// ReviewCompare — Side-by-Side Duplicate Comparison
// =============================================================================
interface ReviewCompareProps {
ctrl: CanonicalControl
onBack: () => void
onReview: (controlId: string, action: string) => void
onEdit: () => void
reviewIndex: number
reviewTotal: number
onReviewPrev: () => void
onReviewNext: () => void
}
export function ReviewCompare({
ctrl,
onBack,
onReview,
onEdit,
reviewIndex,
reviewTotal,
onReviewPrev,
onReviewNext,
}: ReviewCompareProps) {
const [suspectedDuplicate, setSuspectedDuplicate] = useState(null)
const [loading, setLoading] = useState(false)
const [similarity, setSimilarity] = useState(null)
// Load the suspected duplicate from generation_metadata.similar_controls
useEffect(() => {
const loadDuplicate = async () => {
const similarControls = ctrl.generation_metadata?.similar_controls as Array<{ control_id: string; title: string; similarity: number }> | undefined
if (!similarControls || similarControls.length === 0) {
setSuspectedDuplicate(null)
setSimilarity(null)
return
}
const suspect = similarControls[0]
setSimilarity(suspect.similarity)
setLoading(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${encodeURIComponent(suspect.control_id)}`)
if (res.ok) {
const data = await res.json()
setSuspectedDuplicate(data)
} else {
setSuspectedDuplicate(null)
}
} catch {
setSuspectedDuplicate(null)
} finally {
setLoading(false)
}
}
loadDuplicate()
}, [ctrl.control_id, ctrl.generation_metadata])
return (
{/* Header */}
Duplikat-Vergleich
{similarity !== null && (
{(similarity * 100).toFixed(1)}% Aehnlichkeit
)}
{/* Navigation */}
{reviewIndex + 1} / {reviewTotal}
{/* Actions */}
{/* Side-by-Side Panels */}
{/* Left: Control to review */}
{/* Right: Suspected duplicate */}
{loading ? (
) : suspectedDuplicate ? (
) : (
Kein Duplikat-Kandidat gefunden
)}
)
}