Export Report ({label})
{/* BCF Export Dialog (controlled open) */}
sum + s.failedCount, 0) ?? 0}
onExport={onExportBCF}
progress={bcfExportProgress}
open={bcfDialogOpen}
onOpenChange={setBcfDialogOpen}
/>
>
);
}
// ============================================================================
// Main Panel Component
// ============================================================================
export function IDSPanel({ onClose }: IDSPanelProps) {
const fileInputRef = useRef(null);
const {
// State
document,
auditReport,
auditing,
report,
loading,
progress,
error,
activeSpecificationId,
filterMode,
isolationScope,
isolateMode,
isolationActive,
// Actions
loadIDSFile,
clearIDS,
runValidation,
clearValidation,
setActiveSpecification,
selectEntity,
setFilterMode,
setIsolationScope,
applyColors,
isolateFailed,
isolatePassed,
isolateInvolved,
clearIsolation,
exportReportJSON,
exportReportHTML,
exportReportBCF,
bcfExportProgress,
} = useIDS();
// Handle file selection
const handleFileSelect = useCallback(async (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (file) {
await loadIDSFile(file);
}
// Reset input for re-selection of same file
e.target.value = '';
}, [loadIDSFile]);
const loadIdsFromDialog = useCallback(async (): Promise => {
const file = await openGenericFileDialog({
title: 'Open IDS File',
filters: [
{ name: 'IDS Files', extensions: ['ids', 'xml'] },
{ name: 'All Files', extensions: ['*'] },
],
});
if (file) {
await loadIDSFile(file);
return true;
}
return false;
}, [loadIDSFile]);
const handleLoadIdsClick = useCallback(async () => {
const loaded = await loadIdsFromDialog();
if (loaded) {
return;
}
fileInputRef.current?.click();
}, [loadIdsFromDialog]);
// Handle entity click
const handleEntityClick = useCallback((modelId: string, expressId: number) => {
selectEntity(modelId, expressId);
}, [selectEntity]);
// Active state for the isolate toggle buttons. A button is "active" only
// when ITS mode is applied AND isolation is still live, so an externally
// cleared isolation self-heals the button back to inactive.
const failedActive = isolationActive && isolateMode === 'failed';
const passedActive = isolationActive && isolateMode === 'passed';
const involvedActive = isolationActive && isolateMode === 'involved';
// Clicking the active isolate button toggles it off (undo).
const handleIsolateFailed = useCallback(() => {
if (failedActive) clearIsolation();
else isolateFailed();
}, [failedActive, clearIsolation, isolateFailed]);
const handleIsolatePassed = useCallback(() => {
if (passedActive) clearIsolation();
else isolatePassed();
}, [passedActive, clearIsolation, isolatePassed]);
const handleIsolateInvolved = useCallback(() => {
if (involvedActive) clearIsolation();
else isolateInvolved();
}, [involvedActive, clearIsolation, isolateInvolved]);
// Render validation progress
const renderProgress = () => {
if (!progress) return null;
// Validation of large code-list IDS packs runs for many seconds, and
// a few broad specs dominate the time — so a percentage keyed on spec
// index sits near 0 for a while. Surface the always-advancing spec
// counter (and the per-spec entity count) so the panel visibly moves
// throughout, not just in the back half.
const specNumber = Math.min(progress.specificationIndex + 1, progress.totalSpecifications);
const isComplete = progress.phase === 'complete';
const headline = isComplete
? 'Validation complete'
: `Validating specification ${specNumber} of ${progress.totalSpecifications}`;
const detail =
progress.phase === 'validating' && progress.totalEntities > 0
? `Checking ${progress.entitiesProcessed.toLocaleString()} / ${progress.totalEntities.toLocaleString()} entities`
: progress.phase === 'filtering' && progress.totalEntities > 0
? `Scanning ${progress.entitiesProcessed.toLocaleString()} / ${progress.totalEntities.toLocaleString()} candidates`
: progress.phase === 'filtering'
? 'Finding applicable entities…'
: null;
return (
{!isComplete && }
{headline}
{detail &&
{detail}
}
);
};
// Render empty state
const renderEmptyState = () => {
if (document) return null;
// When parse failed but the auditor still produced issues, surface
// them here. This is the most common path for malformed input —
// bare "Invalid XML format" tells the user nothing actionable, but
// the audit lists the specific structural problems.
const hasAuditIssues =
auditReport !== null && auditReport.issues.length > 0;
return (
{hasAuditIssues && (
)}
{hasAuditIssues ? 'IDS Document Has Errors' : 'No IDS Loaded'}
{hasAuditIssues
? 'Fix the issues above and try loading again.'
: 'Load an IDS (Information Delivery Specification) file to validate your model'}
);
};
// Render document loaded but no validation
const renderDocumentLoaded = () => {
if (!document || report) return null;
// Only the document-level auditor's `error` verdict gates model
// validation — warnings still let the user proceed (they're style
// hints, not blockers). The button keeps its primary affordance
// unless we genuinely can't validate.
const auditHasErrors = auditReport?.status === 'error';
return (
{specScope
? '💡 Select a specification to isolate its elements — passed green, failed red'
: '💡 Click any entity to select and zoom to it in the 3D view'}
{/* Filter & Actions Bar */}
{/* Isolate scope: whole report vs. the active specification (#1236).
'Per Spec' isolates the selected specification's elements
(passed green, failed red). */}
{failedActive ? 'Show all (undo isolate failed)' : `Isolate failed${scopeSuffix}`}
{passedActive ? 'Show all (undo isolate passed)' : `Isolate passed${scopeSuffix}`}
{involvedActive
? 'Show all (undo isolate involved)'
: `Isolate involved${scopeSuffix} — passed green + failed red`}
Clear isolation (show all)Reapply Colors