@ifc-lite/viewer 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.turbo/turbo-build.log +59 -43
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +496 -0
  4. package/dist/assets/basketViewActivator-Bzw51jhm.js +71 -0
  5. package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
  6. package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
  7. package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
  8. package/dist/assets/exporters-u0sz2Upj.js +259119 -0
  9. package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
  10. package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
  11. package/dist/assets/ids-B7AXEv7h.js +4067 -0
  12. package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
  13. package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
  14. package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
  15. package/dist/assets/index-CSWgTe1s.css +1 -0
  16. package/dist/assets/{index-BOi3BuUI.js → index-DVNSvEMh.js} +49877 -28410
  17. package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
  18. package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
  19. package/dist/assets/{native-bridge-CpBeOPQa.js → native-bridge-BiD01jI9.js} +2 -2
  20. package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
  21. package/dist/assets/{sandbox-Baez7n-t.js → sandbox-DPD1ROr0.js} +548 -530
  22. package/dist/assets/{server-client-BB6cMAXE.js → server-client-DP8fMPY9.js} +1 -1
  23. package/dist/assets/three-CDRZThFA.js +4057 -0
  24. package/dist/assets/{wasm-bridge-CAYCUHbE.js → wasm-bridge-CErti6zX.js} +1 -1
  25. package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
  26. package/dist/index.html +10 -9
  27. package/dist/samples/building-architecture.ifc +453 -0
  28. package/dist/samples/hello-wall.ifc +1054 -0
  29. package/dist/samples/infra-bridge.ifc +962 -0
  30. package/index.html +1 -1
  31. package/package.json +15 -10
  32. package/public/samples/building-architecture.ifc +453 -0
  33. package/public/samples/hello-wall.ifc +1054 -0
  34. package/public/samples/infra-bridge.ifc +962 -0
  35. package/src/App.tsx +37 -3
  36. package/src/components/mcp/HeroScene.tsx +876 -0
  37. package/src/components/mcp/McpLanding.tsx +1318 -0
  38. package/src/components/mcp/McpPlayground.tsx +524 -0
  39. package/src/components/mcp/PlaygroundChat.tsx +1097 -0
  40. package/src/components/mcp/PlaygroundViewer.tsx +815 -0
  41. package/src/components/mcp/README.md +171 -0
  42. package/src/components/mcp/data.ts +659 -0
  43. package/src/components/mcp/playground-dispatcher.ts +1649 -0
  44. package/src/components/mcp/playground-files.ts +107 -0
  45. package/src/components/mcp/playground-uploads.ts +122 -0
  46. package/src/components/mcp/types.ts +65 -0
  47. package/src/components/mcp/use-mcp-page.ts +109 -0
  48. package/src/components/viewer/BasketPresentationDock.tsx +3 -0
  49. package/src/components/viewer/CesiumOverlay.tsx +165 -120
  50. package/src/components/viewer/DeviationPanel.tsx +172 -0
  51. package/src/components/viewer/HierarchyPanel.tsx +29 -3
  52. package/src/components/viewer/HoverTooltip.tsx +5 -0
  53. package/src/components/viewer/IDSAuditSummary.tsx +389 -0
  54. package/src/components/viewer/IDSPanel.tsx +80 -26
  55. package/src/components/viewer/MainToolbar.tsx +79 -7
  56. package/src/components/viewer/MergeLayersBanner.tsx +108 -0
  57. package/src/components/viewer/MobileToolbar.tsx +326 -0
  58. package/src/components/viewer/PointCloudClasses.tsx +111 -0
  59. package/src/components/viewer/PointCloudLegend.tsx +119 -0
  60. package/src/components/viewer/PointCloudPanel.tsx +52 -1
  61. package/src/components/viewer/PropertiesPanel.tsx +37 -6
  62. package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
  63. package/src/components/viewer/StatusBar.tsx +14 -0
  64. package/src/components/viewer/ViewerLayout.tsx +288 -95
  65. package/src/components/viewer/Viewport.tsx +86 -18
  66. package/src/components/viewer/ViewportContainer.tsx +60 -15
  67. package/src/components/viewer/ViewportOverlays.tsx +41 -26
  68. package/src/components/viewer/mouseHandlerTypes.ts +22 -0
  69. package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
  70. package/src/components/viewer/properties/MaterialCard.tsx +2 -2
  71. package/src/components/viewer/selectionHandlers.ts +41 -0
  72. package/src/components/viewer/tools/SectionPanel.tsx +181 -24
  73. package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
  74. package/src/components/viewer/useAnimationLoop.ts +22 -0
  75. package/src/components/viewer/useMouseControls.ts +296 -3
  76. package/src/components/viewer/usePointCloudSync.ts +8 -1
  77. package/src/components/viewer/useRenderUpdates.ts +21 -1
  78. package/src/components/viewer/useTouchControls.ts +100 -41
  79. package/src/generated/mcp-catalog.json +82 -0
  80. package/src/hooks/federationLoadGate.test.ts +90 -0
  81. package/src/hooks/federationLoadGate.ts +127 -0
  82. package/src/hooks/ids/idsDataAccessor.ts +11 -259
  83. package/src/hooks/ingest/pointCloudIngest.ts +127 -16
  84. package/src/hooks/useDrawingGeneration.ts +81 -8
  85. package/src/hooks/useIDS.ts +90 -10
  86. package/src/hooks/useIfcFederation.ts +94 -16
  87. package/src/hooks/useIfcLoader.ts +289 -64
  88. package/src/hooks/useViewerSelectors.ts +10 -0
  89. package/src/lib/geo/cesium-bridge.ts +84 -67
  90. package/src/lib/geo/clamp-anchor.test.ts +80 -0
  91. package/src/lib/geo/clamp-anchor.ts +57 -0
  92. package/src/lib/geo/effective-georef.test.ts +79 -1
  93. package/src/lib/geo/effective-georef.ts +83 -0
  94. package/src/lib/geo/reproject.ts +26 -13
  95. package/src/lib/geo/terrain-elevation.ts +166 -0
  96. package/src/lib/lens/adapter.ts +1 -1
  97. package/src/lib/llm/context-builder.ts +1 -1
  98. package/src/lib/perf/memoryAccounting.test.ts +92 -0
  99. package/src/lib/perf/memoryAccounting.ts +235 -0
  100. package/src/sdk/adapters/mutation-view.ts +1 -1
  101. package/src/store/constants.ts +39 -2
  102. package/src/store/index.ts +6 -1
  103. package/src/store/slices/cesiumSlice.ts +1 -1
  104. package/src/store/slices/idsSlice.ts +24 -0
  105. package/src/store/slices/loadingSlice.ts +12 -0
  106. package/src/store/slices/pointCloudSlice.ts +72 -1
  107. package/src/store/slices/sectionSlice.test.ts +590 -1
  108. package/src/store/slices/sectionSlice.ts +344 -17
  109. package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
  110. package/src/store/slices/uiSlice.ts +60 -2
  111. package/src/store/types.ts +42 -0
  112. package/src/store.ts +13 -0
  113. package/src/utils/acquireFileBuffer.test.ts +231 -0
  114. package/src/utils/acquireFileBuffer.ts +128 -0
  115. package/src/utils/ifcConfig.ts +24 -0
  116. package/src/utils/nativeSpatialDataStore.ts +20 -2
  117. package/src/utils/spatialHierarchy.test.ts +116 -0
  118. package/src/utils/spatialHierarchy.ts +23 -0
  119. package/tailwind.config.js +5 -0
  120. package/tsconfig.json +1 -0
  121. package/vite.config.ts +12 -0
  122. package/dist/assets/basketViewActivator-RZy5c3Td.js +0 -1
  123. package/dist/assets/decode-worker-Collf_X_.js +0 -1320
  124. package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
  125. package/dist/assets/exporters-BraHBeoi.js +0 -81583
  126. package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
  127. package/dist/assets/ids-DQ5jY0E8.js +0 -1
  128. package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
  129. package/dist/assets/index-0XpVr_S5.css +0 -1
@@ -0,0 +1,389 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * IDSAuditSummary — surfaces the auditor's verdict on a loaded IDS
7
+ * document.
8
+ *
9
+ * Visual language: refined-technical instrument. Restraint over
10
+ * decoration. The hierarchy is carried by:
11
+ * - **Severity rails** — 2px tinted left border on each issue row.
12
+ * - **Codes as machine output** — monospace uppercase chips with
13
+ * severity-tinted backgrounds; treat them like log lines.
14
+ * - **Counts strip** — compact `▪ 3 errors ▪ 2 warnings ▪ 0 info`
15
+ * bar with colored dots, similar to a developer-tool status line.
16
+ * - **Empty state** — single line with a check icon, no flair.
17
+ *
18
+ * Interactions:
19
+ * - Click counts strip to toggle the issue list.
20
+ * - Click an individual row to expose its `path` and `detail` payload.
21
+ * - Filter tabs (All / Errors / Warnings) when any issues exist.
22
+ */
23
+
24
+ import React, { useMemo, useState } from 'react';
25
+ import {
26
+ AlertCircle,
27
+ AlertTriangle,
28
+ CheckCircle2,
29
+ ChevronDown,
30
+ ChevronRight,
31
+ Info,
32
+ Loader2,
33
+ } from 'lucide-react';
34
+ import type { IDSAuditIssue, IDSAuditReport, IDSAuditSeverity } from '@ifc-lite/ids';
35
+ import { cn } from '@/lib/utils';
36
+
37
+ interface IDSAuditSummaryProps {
38
+ report: IDSAuditReport | null;
39
+ /** True while the auditor is running. */
40
+ auditing?: boolean;
41
+ /** Optional className passed to the outer container. */
42
+ className?: string;
43
+ }
44
+
45
+ type SeverityFilter = 'all' | IDSAuditSeverity;
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Severity tokens
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const SEVERITY_ORDER: Record<IDSAuditSeverity, number> = {
52
+ error: 0,
53
+ warning: 1,
54
+ info: 2,
55
+ };
56
+
57
+ const SEVERITY_TOKENS: Record<
58
+ IDSAuditSeverity,
59
+ {
60
+ label: string;
61
+ pluralLabel: string;
62
+ dot: string;
63
+ rail: string;
64
+ chipBg: string;
65
+ chipFg: string;
66
+ chipBorder: string;
67
+ icon: React.ReactNode;
68
+ iconClass: string;
69
+ }
70
+ > = {
71
+ error: {
72
+ label: 'error',
73
+ pluralLabel: 'errors',
74
+ dot: 'bg-red-500',
75
+ rail: 'border-l-red-500',
76
+ chipBg: 'bg-red-500/10',
77
+ chipFg: 'text-red-600 dark:text-red-400',
78
+ chipBorder: 'border-red-500/30',
79
+ icon: <AlertCircle className="h-4 w-4" aria-hidden="true" />,
80
+ iconClass: 'text-red-500',
81
+ },
82
+ warning: {
83
+ label: 'warning',
84
+ pluralLabel: 'warnings',
85
+ dot: 'bg-amber-500',
86
+ rail: 'border-l-amber-500',
87
+ chipBg: 'bg-amber-500/10',
88
+ chipFg: 'text-amber-700 dark:text-amber-400',
89
+ chipBorder: 'border-amber-500/30',
90
+ icon: <AlertTriangle className="h-4 w-4" aria-hidden="true" />,
91
+ iconClass: 'text-amber-500',
92
+ },
93
+ info: {
94
+ label: 'note',
95
+ pluralLabel: 'notes',
96
+ dot: 'bg-sky-400',
97
+ rail: 'border-l-sky-400',
98
+ chipBg: 'bg-sky-400/10',
99
+ chipFg: 'text-sky-600 dark:text-sky-400',
100
+ chipBorder: 'border-sky-400/30',
101
+ icon: <Info className="h-4 w-4" aria-hidden="true" />,
102
+ iconClass: 'text-sky-500',
103
+ },
104
+ };
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Component
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export function IDSAuditSummary({
111
+ report,
112
+ auditing = false,
113
+ className,
114
+ }: IDSAuditSummaryProps): JSX.Element | null {
115
+ const [expanded, setExpanded] = useState(false);
116
+ const [filter, setFilter] = useState<SeverityFilter>('all');
117
+
118
+ // Stable per-severity counts.
119
+ const counts = useMemo(() => {
120
+ const base: Record<IDSAuditSeverity, number> = {
121
+ error: 0,
122
+ warning: 0,
123
+ info: 0,
124
+ };
125
+ if (!report) return base;
126
+ for (const issue of report.issues) {
127
+ base[issue.severity] += 1;
128
+ }
129
+ return base;
130
+ }, [report]);
131
+
132
+ // Sort issues by severity (errors first), preserving document order
133
+ // within each bucket. Rendering errors-first gives the user the most
134
+ // important information at the top of the expanded list.
135
+ const sortedIssues = useMemo(() => {
136
+ if (!report) return [];
137
+ return [...report.issues].sort(
138
+ (a, b) =>
139
+ SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
140
+ );
141
+ }, [report]);
142
+
143
+ const visibleIssues = useMemo(() => {
144
+ if (filter === 'all') return sortedIssues;
145
+ return sortedIssues.filter((i) => i.severity === filter);
146
+ }, [sortedIssues, filter]);
147
+
148
+ // Auditing in flight — quietly mark the spot.
149
+ if (auditing && !report) {
150
+ return (
151
+ <div
152
+ className={cn(
153
+ 'flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground',
154
+ 'animate-fade-in-up',
155
+ className
156
+ )}
157
+ role="status"
158
+ aria-live="polite"
159
+ >
160
+ <Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
161
+ <span>Auditing IDS document…</span>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ if (!report) return null;
167
+
168
+ const totalIssues = report.issues.length;
169
+ const isClean = report.status === 'valid' || totalIssues === 0;
170
+
171
+ // Clean state — single line, no flair.
172
+ if (isClean) {
173
+ return (
174
+ <div
175
+ className={cn(
176
+ 'flex items-center gap-2 rounded-md border border-emerald-500/30 bg-emerald-500/5 px-3 py-2 text-xs text-emerald-700 dark:text-emerald-400',
177
+ 'animate-fade-in-up',
178
+ className
179
+ )}
180
+ >
181
+ <CheckCircle2 className="h-4 w-4" aria-hidden="true" />
182
+ <span>Document is valid — no audit issues</span>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ // Has issues — counts strip + collapsible list.
188
+ return (
189
+ <section
190
+ className={cn(
191
+ 'overflow-hidden rounded-md border border-border/70 bg-card animate-fade-in-up',
192
+ className
193
+ )}
194
+ aria-label="IDS document audit summary"
195
+ >
196
+ <button
197
+ type="button"
198
+ onClick={() => setExpanded((v) => !v)}
199
+ className={cn(
200
+ 'flex w-full items-center justify-between gap-3 px-3 py-2 text-left transition-colors',
201
+ 'hover:bg-muted/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60'
202
+ )}
203
+ aria-expanded={expanded}
204
+ >
205
+ <span className="flex items-center gap-3 text-xs">
206
+ {(['error', 'warning', 'info'] as IDSAuditSeverity[]).map((sev) => {
207
+ const n = counts[sev];
208
+ if (n === 0) return null;
209
+ const t = SEVERITY_TOKENS[sev];
210
+ return (
211
+ <span key={sev} className="inline-flex items-center gap-1.5">
212
+ <span
213
+ className={cn('h-1.5 w-1.5 rounded-full', t.dot)}
214
+ aria-hidden="true"
215
+ />
216
+ <span className={cn('font-mono tabular-nums', t.chipFg)}>
217
+ {n}
218
+ </span>
219
+ <span className="text-muted-foreground">
220
+ {n === 1 ? t.label : t.pluralLabel}
221
+ </span>
222
+ </span>
223
+ );
224
+ })}
225
+ </span>
226
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
227
+ <span>{expanded ? 'Hide' : 'Details'}</span>
228
+ {expanded ? (
229
+ <ChevronDown className="h-3.5 w-3.5" aria-hidden="true" />
230
+ ) : (
231
+ <ChevronRight className="h-3.5 w-3.5" aria-hidden="true" />
232
+ )}
233
+ </span>
234
+ </button>
235
+
236
+ {expanded && (
237
+ <div className="border-t border-border/60">
238
+ {/* Filter tabs */}
239
+ <div className="flex items-center gap-1 border-b border-border/60 bg-muted/20 px-2 py-1.5">
240
+ {(
241
+ [
242
+ { key: 'all', label: `All (${totalIssues})` },
243
+ counts.error > 0 && {
244
+ key: 'error',
245
+ label: `Errors (${counts.error})`,
246
+ },
247
+ counts.warning > 0 && {
248
+ key: 'warning',
249
+ label: `Warnings (${counts.warning})`,
250
+ },
251
+ counts.info > 0 && {
252
+ key: 'info',
253
+ label: `Notes (${counts.info})`,
254
+ },
255
+ ].filter(Boolean) as Array<{
256
+ key: SeverityFilter;
257
+ label: string;
258
+ }>
259
+ ).map((tab) => (
260
+ <button
261
+ key={tab.key}
262
+ type="button"
263
+ onClick={() => setFilter(tab.key)}
264
+ className={cn(
265
+ 'rounded px-2 py-0.5 text-[11px] transition-colors',
266
+ filter === tab.key
267
+ ? 'bg-foreground text-background'
268
+ : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
269
+ )}
270
+ >
271
+ {tab.label}
272
+ </button>
273
+ ))}
274
+ </div>
275
+
276
+ {/* Issue list */}
277
+ <ul className="max-h-72 overflow-y-auto py-1">
278
+ {visibleIssues.map((issue, i) => (
279
+ <IssueRow key={`${issue.code}-${issue.path}-${i}`} issue={issue} index={i} />
280
+ ))}
281
+ {visibleIssues.length === 0 && (
282
+ <li className="px-3 py-3 text-xs text-muted-foreground">
283
+ No issues match the selected filter.
284
+ </li>
285
+ )}
286
+ </ul>
287
+ </div>
288
+ )}
289
+ </section>
290
+ );
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Issue row
295
+ // ---------------------------------------------------------------------------
296
+
297
+ interface IssueRowProps {
298
+ issue: IDSAuditIssue;
299
+ index: number;
300
+ }
301
+
302
+ function IssueRow({ issue, index }: IssueRowProps): JSX.Element {
303
+ const [open, setOpen] = useState(false);
304
+ const t = SEVERITY_TOKENS[issue.severity];
305
+ const hasDetail =
306
+ !!issue.path ||
307
+ (issue.detail !== undefined && Object.keys(issue.detail).length > 0);
308
+
309
+ return (
310
+ <li
311
+ className={cn(
312
+ 'group border-l-2 px-3 py-1.5 text-xs transition-colors hover:bg-muted/30',
313
+ t.rail,
314
+ // Stagger reveal — capped so long lists don't take seconds.
315
+ 'animate-fade-in-up'
316
+ )}
317
+ style={{ animationDelay: `${Math.min(index, 12) * 24}ms` }}
318
+ >
319
+ <button
320
+ type="button"
321
+ onClick={() => hasDetail && setOpen((v) => !v)}
322
+ className={cn(
323
+ 'flex w-full items-start gap-2 text-left',
324
+ hasDetail && 'cursor-pointer',
325
+ !hasDetail && 'cursor-default'
326
+ )}
327
+ aria-expanded={hasDetail ? open : undefined}
328
+ >
329
+ <span className={cn('mt-0.5 shrink-0', t.iconClass)}>{t.icon}</span>
330
+ <span className="min-w-0 flex-1 space-y-1">
331
+ <span className="flex flex-wrap items-baseline gap-2">
332
+ <code
333
+ className={cn(
334
+ 'shrink-0 rounded border px-1.5 py-0 font-mono text-[10px] uppercase tracking-tight leading-relaxed',
335
+ t.chipBg,
336
+ t.chipFg,
337
+ t.chipBorder
338
+ )}
339
+ >
340
+ {issue.code}
341
+ </code>
342
+ <span className="text-foreground">{issue.message}</span>
343
+ </span>
344
+ {hasDetail && open && (
345
+ <div className="ml-1 mt-1.5 space-y-1 border-l border-border/60 pl-2">
346
+ {issue.path && (
347
+ <div className="flex gap-2 font-mono text-[11px]">
348
+ <span className="text-muted-foreground/70">path</span>
349
+ <span className="break-all text-muted-foreground">
350
+ {issue.path}
351
+ </span>
352
+ </div>
353
+ )}
354
+ {issue.facetType && (
355
+ <div className="flex gap-2 font-mono text-[11px]">
356
+ <span className="text-muted-foreground/70">facet</span>
357
+ <span className="text-muted-foreground">
358
+ {issue.facetType}
359
+ </span>
360
+ </div>
361
+ )}
362
+ {issue.detail && Object.keys(issue.detail).length > 0 && (
363
+ <div className="flex flex-col gap-0.5 font-mono text-[11px]">
364
+ {Object.entries(issue.detail).map(([k, v]) => (
365
+ <div key={k} className="flex gap-2">
366
+ <span className="text-muted-foreground/70">{k}</span>
367
+ <span className="break-all text-muted-foreground">
368
+ {String(v)}
369
+ </span>
370
+ </div>
371
+ ))}
372
+ </div>
373
+ )}
374
+ </div>
375
+ )}
376
+ </span>
377
+ {hasDetail && (
378
+ <ChevronDown
379
+ className={cn(
380
+ 'mt-1 h-3 w-3 shrink-0 text-muted-foreground/60 transition-transform',
381
+ open && 'rotate-180'
382
+ )}
383
+ aria-hidden="true"
384
+ />
385
+ )}
386
+ </button>
387
+ </li>
388
+ );
389
+ }
@@ -72,6 +72,7 @@ import type {
72
72
  IDSRequirementResult,
73
73
  } from '@ifc-lite/ids';
74
74
  import { cn } from '@/lib/utils';
75
+ import { IDSAuditSummary } from './IDSAuditSummary';
75
76
  import { IDSExportDialog } from './IDSExportDialog';
76
77
  import type { IDSBCFExportSettings, IDSExportProgress } from './IDSExportDialog';
77
78
  import { claimNextDesktopPanelAction, subscribeDesktopPanelActions } from '@/services/desktop-panel-actions';
@@ -452,6 +453,8 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
452
453
  const {
453
454
  // State
454
455
  document,
456
+ auditReport,
457
+ auditing,
455
458
  report,
456
459
  loading,
457
460
  progress,
@@ -562,24 +565,43 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
562
565
  const renderEmptyState = () => {
563
566
  if (document) return null;
564
567
 
568
+ // When parse failed but the auditor still produced issues, surface
569
+ // them here. This is the most common path for malformed input —
570
+ // bare "Invalid XML format" tells the user nothing actionable, but
571
+ // the audit lists the specific structural problems.
572
+ const hasAuditIssues =
573
+ auditReport !== null && auditReport.issues.length > 0;
574
+
565
575
  return (
566
- <div className="flex flex-col items-center justify-center h-full p-6 text-center">
567
- <FileText className="h-12 w-12 text-muted-foreground mb-4" />
568
- <h3 className="font-medium text-sm mb-2">No IDS Loaded</h3>
569
- <p className="text-xs text-muted-foreground mb-4">
570
- Load an IDS (Information Delivery Specification) file to validate your model
571
- </p>
572
- <input
573
- ref={fileInputRef}
574
- type="file"
575
- accept=".ids,.xml"
576
- className="hidden"
577
- onChange={handleFileSelect}
578
- />
579
- <Button onClick={() => { void handleLoadIdsClick(); }}>
580
- <Upload className="h-4 w-4 mr-2" />
581
- Load IDS File
582
- </Button>
576
+ <div className="flex flex-col h-full p-6">
577
+ {hasAuditIssues && (
578
+ <div className="mb-4">
579
+ <IDSAuditSummary report={auditReport} auditing={auditing} />
580
+ </div>
581
+ )}
582
+
583
+ <div className="flex flex-col items-center justify-center flex-1 text-center">
584
+ <FileText className="h-12 w-12 text-muted-foreground mb-4" />
585
+ <h3 className="font-medium text-sm mb-2">
586
+ {hasAuditIssues ? 'IDS Document Has Errors' : 'No IDS Loaded'}
587
+ </h3>
588
+ <p className="text-xs text-muted-foreground mb-4">
589
+ {hasAuditIssues
590
+ ? 'Fix the issues above and try loading again.'
591
+ : 'Load an IDS (Information Delivery Specification) file to validate your model'}
592
+ </p>
593
+ <input
594
+ ref={fileInputRef}
595
+ type="file"
596
+ accept=".ids,.xml"
597
+ className="hidden"
598
+ onChange={handleFileSelect}
599
+ />
600
+ <Button onClick={() => { void handleLoadIdsClick(); }}>
601
+ <Upload className="h-4 w-4 mr-2" />
602
+ {hasAuditIssues ? 'Load Different File' : 'Load IDS File'}
603
+ </Button>
604
+ </div>
583
605
  </div>
584
606
  );
585
607
  };
@@ -588,9 +610,15 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
588
610
  const renderDocumentLoaded = () => {
589
611
  if (!document || report) return null;
590
612
 
613
+ // Only the document-level auditor's `error` verdict gates model
614
+ // validation — warnings still let the user proceed (they're style
615
+ // hints, not blockers). The button keeps its primary affordance
616
+ // unless we genuinely can't validate.
617
+ const auditHasErrors = auditReport?.status === 'error';
618
+
591
619
  return (
592
- <div className="p-4">
593
- <div className="rounded-lg border p-4 mb-4">
620
+ <div className="p-4 space-y-3">
621
+ <div className="rounded-lg border p-4">
594
622
  <h3 className="font-medium text-sm mb-1">{document.info.title}</h3>
595
623
  {document.info.description && (
596
624
  <p className="text-xs text-muted-foreground mb-2">{document.info.description}</p>
@@ -601,14 +629,32 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
601
629
  </div>
602
630
  </div>
603
631
 
604
- <Button className="w-full" onClick={runValidation} disabled={loading}>
605
- {loading ? (
606
- <Loader2 className="h-4 w-4 mr-2 animate-spin" />
607
- ) : (
608
- <Play className="h-4 w-4 mr-2" />
632
+ <IDSAuditSummary report={auditReport} auditing={auditing} />
633
+
634
+ <Tooltip>
635
+ <TooltipTrigger asChild>
636
+ <span className="block">
637
+ <Button
638
+ className="w-full"
639
+ onClick={runValidation}
640
+ disabled={loading || auditHasErrors}
641
+ variant={auditHasErrors ? 'secondary' : 'default'}
642
+ >
643
+ {loading ? (
644
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
645
+ ) : (
646
+ <Play className="h-4 w-4 mr-2" />
647
+ )}
648
+ Run Validation
649
+ </Button>
650
+ </span>
651
+ </TooltipTrigger>
652
+ {auditHasErrors && (
653
+ <TooltipContent>
654
+ Resolve audit errors before validating against a model.
655
+ </TooltipContent>
609
656
  )}
610
- Run Validation
611
- </Button>
657
+ </Tooltip>
612
658
  </div>
613
659
  );
614
660
  };
@@ -619,6 +665,14 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
619
665
 
620
666
  return (
621
667
  <>
668
+ {/* Audit summary stays visible above the validation report so
669
+ users can still see authoring issues alongside model results. */}
670
+ {auditReport && auditReport.status !== 'valid' && (
671
+ <div className="p-3 border-b">
672
+ <IDSAuditSummary report={auditReport} auditing={false} />
673
+ </div>
674
+ )}
675
+
622
676
  {/* Summary Header */}
623
677
  <div className="p-3 border-b bg-muted/30">
624
678
  <div className="flex items-center gap-2 mb-2">