@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.
- package/.turbo/turbo-build.log +59 -43
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +496 -0
- package/dist/assets/basketViewActivator-Bzw51jhm.js +71 -0
- package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
- package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
- package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
- package/dist/assets/exporters-u0sz2Upj.js +259119 -0
- package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
- package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
- package/dist/assets/ids-B7AXEv7h.js +4067 -0
- package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
- package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
- package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
- package/dist/assets/index-CSWgTe1s.css +1 -0
- package/dist/assets/{index-BOi3BuUI.js → index-DVNSvEMh.js} +49877 -28410
- package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
- package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
- package/dist/assets/{native-bridge-CpBeOPQa.js → native-bridge-BiD01jI9.js} +2 -2
- package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
- package/dist/assets/{sandbox-Baez7n-t.js → sandbox-DPD1ROr0.js} +548 -530
- package/dist/assets/{server-client-BB6cMAXE.js → server-client-DP8fMPY9.js} +1 -1
- package/dist/assets/three-CDRZThFA.js +4057 -0
- package/dist/assets/{wasm-bridge-CAYCUHbE.js → wasm-bridge-CErti6zX.js} +1 -1
- package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
- package/dist/index.html +10 -9
- package/dist/samples/building-architecture.ifc +453 -0
- package/dist/samples/hello-wall.ifc +1054 -0
- package/dist/samples/infra-bridge.ifc +962 -0
- package/index.html +1 -1
- package/package.json +15 -10
- package/public/samples/building-architecture.ifc +453 -0
- package/public/samples/hello-wall.ifc +1054 -0
- package/public/samples/infra-bridge.ifc +962 -0
- package/src/App.tsx +37 -3
- package/src/components/mcp/HeroScene.tsx +876 -0
- package/src/components/mcp/McpLanding.tsx +1318 -0
- package/src/components/mcp/McpPlayground.tsx +524 -0
- package/src/components/mcp/PlaygroundChat.tsx +1097 -0
- package/src/components/mcp/PlaygroundViewer.tsx +815 -0
- package/src/components/mcp/README.md +171 -0
- package/src/components/mcp/data.ts +659 -0
- package/src/components/mcp/playground-dispatcher.ts +1649 -0
- package/src/components/mcp/playground-files.ts +107 -0
- package/src/components/mcp/playground-uploads.ts +122 -0
- package/src/components/mcp/types.ts +65 -0
- package/src/components/mcp/use-mcp-page.ts +109 -0
- package/src/components/viewer/BasketPresentationDock.tsx +3 -0
- package/src/components/viewer/CesiumOverlay.tsx +165 -120
- package/src/components/viewer/DeviationPanel.tsx +172 -0
- package/src/components/viewer/HierarchyPanel.tsx +29 -3
- package/src/components/viewer/HoverTooltip.tsx +5 -0
- package/src/components/viewer/IDSAuditSummary.tsx +389 -0
- package/src/components/viewer/IDSPanel.tsx +80 -26
- package/src/components/viewer/MainToolbar.tsx +79 -7
- package/src/components/viewer/MergeLayersBanner.tsx +108 -0
- package/src/components/viewer/MobileToolbar.tsx +326 -0
- package/src/components/viewer/PointCloudClasses.tsx +111 -0
- package/src/components/viewer/PointCloudLegend.tsx +119 -0
- package/src/components/viewer/PointCloudPanel.tsx +52 -1
- package/src/components/viewer/PropertiesPanel.tsx +37 -6
- package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
- package/src/components/viewer/StatusBar.tsx +14 -0
- package/src/components/viewer/ViewerLayout.tsx +288 -95
- package/src/components/viewer/Viewport.tsx +86 -18
- package/src/components/viewer/ViewportContainer.tsx +60 -15
- package/src/components/viewer/ViewportOverlays.tsx +41 -26
- package/src/components/viewer/mouseHandlerTypes.ts +22 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
- package/src/components/viewer/properties/MaterialCard.tsx +2 -2
- package/src/components/viewer/selectionHandlers.ts +41 -0
- package/src/components/viewer/tools/SectionPanel.tsx +181 -24
- package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
- package/src/components/viewer/useAnimationLoop.ts +22 -0
- package/src/components/viewer/useMouseControls.ts +296 -3
- package/src/components/viewer/usePointCloudSync.ts +8 -1
- package/src/components/viewer/useRenderUpdates.ts +21 -1
- package/src/components/viewer/useTouchControls.ts +100 -41
- package/src/generated/mcp-catalog.json +82 -0
- package/src/hooks/federationLoadGate.test.ts +90 -0
- package/src/hooks/federationLoadGate.ts +127 -0
- package/src/hooks/ids/idsDataAccessor.ts +11 -259
- package/src/hooks/ingest/pointCloudIngest.ts +127 -16
- package/src/hooks/useDrawingGeneration.ts +81 -8
- package/src/hooks/useIDS.ts +90 -10
- package/src/hooks/useIfcFederation.ts +94 -16
- package/src/hooks/useIfcLoader.ts +289 -64
- package/src/hooks/useViewerSelectors.ts +10 -0
- package/src/lib/geo/cesium-bridge.ts +84 -67
- package/src/lib/geo/clamp-anchor.test.ts +80 -0
- package/src/lib/geo/clamp-anchor.ts +57 -0
- package/src/lib/geo/effective-georef.test.ts +79 -1
- package/src/lib/geo/effective-georef.ts +83 -0
- package/src/lib/geo/reproject.ts +26 -13
- package/src/lib/geo/terrain-elevation.ts +166 -0
- package/src/lib/lens/adapter.ts +1 -1
- package/src/lib/llm/context-builder.ts +1 -1
- package/src/lib/perf/memoryAccounting.test.ts +92 -0
- package/src/lib/perf/memoryAccounting.ts +235 -0
- package/src/sdk/adapters/mutation-view.ts +1 -1
- package/src/store/constants.ts +39 -2
- package/src/store/index.ts +6 -1
- package/src/store/slices/cesiumSlice.ts +1 -1
- package/src/store/slices/idsSlice.ts +24 -0
- package/src/store/slices/loadingSlice.ts +12 -0
- package/src/store/slices/pointCloudSlice.ts +72 -1
- package/src/store/slices/sectionSlice.test.ts +590 -1
- package/src/store/slices/sectionSlice.ts +344 -17
- package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
- package/src/store/slices/uiSlice.ts +60 -2
- package/src/store/types.ts +42 -0
- package/src/store.ts +13 -0
- package/src/utils/acquireFileBuffer.test.ts +231 -0
- package/src/utils/acquireFileBuffer.ts +128 -0
- package/src/utils/ifcConfig.ts +24 -0
- package/src/utils/nativeSpatialDataStore.ts +20 -2
- package/src/utils/spatialHierarchy.test.ts +116 -0
- package/src/utils/spatialHierarchy.ts +23 -0
- package/tailwind.config.js +5 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +12 -0
- package/dist/assets/basketViewActivator-RZy5c3Td.js +0 -1
- package/dist/assets/decode-worker-Collf_X_.js +0 -1320
- package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
- package/dist/assets/exporters-BraHBeoi.js +0 -81583
- package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
- package/dist/assets/ids-DQ5jY0E8.js +0 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- 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
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
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
|
-
<
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
<
|
|
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
|
-
|
|
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">
|