@fgv/ts-res-browser 5.0.0-2 → 5.0.0-21
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/cli.js +132 -21
- package/dist/bundle.js +1 -1
- package/dist/bundle.js.LICENSE.txt +11 -1
- package/package.json +42 -36
- package/.rush/temp/587277bfc395406ad444e989b6c309b9698f534c.tar.log +0 -15
- package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +0 -35
- package/.rush/temp/operation/build/all.log +0 -35
- package/.rush/temp/operation/build/log-chunks.jsonl +0 -35
- package/.rush/temp/operation/build/state.json +0 -3
- package/.rush/temp/shrinkwrap-deps.json +0 -1225
- package/TS_RES_BROWSER_PLAN.md +0 -689
- package/babel.config.js +0 -17
- package/config/jest.config.json +0 -13
- package/config/rig.json +0 -16
- package/postcss.config.js +0 -3
- package/public/index.html +0 -15
- package/rush-logs/ts-res-browser.build.cache.log +0 -3
- package/rush-logs/ts-res-browser.build.log +0 -35
- package/src/App.tsx +0 -274
- package/src/cli.ts +0 -44
- package/src/components/common/FileImporter.tsx +0 -143
- package/src/components/common/NavigationWarningModal.tsx +0 -62
- package/src/components/layout/AppLayout.tsx +0 -46
- package/src/components/layout/Header.tsx +0 -22
- package/src/components/layout/MessagesWindow.tsx +0 -108
- package/src/components/layout/Sidebar.tsx +0 -99
- package/src/components/tools/CompiledBrowser.tsx +0 -949
- package/src/components/tools/ConfigurationTool.tsx +0 -1405
- package/src/components/tools/FilterTool.tsx +0 -1114
- package/src/components/tools/ImportTool.tsx +0 -249
- package/src/components/tools/ResolutionViewer.tsx +0 -1672
- package/src/components/tools/SourceBrowser.tsx +0 -496
- package/src/components/tools/ZipLoader.tsx +0 -302
- package/src/hooks/useAppState.ts +0 -111
- package/src/hooks/useFileImport.ts +0 -86
- package/src/hooks/useNavigationWarning.ts +0 -67
- package/src/hooks/useResourceManager.ts +0 -295
- package/src/hooks/useUrlParams.ts +0 -56
- package/src/index.css +0 -38
- package/src/main.tsx +0 -16
- package/src/types/app.ts +0 -70
- package/src/types/fileSystemAccess.d.ts +0 -94
- package/src/utils/browserFileTreeAccessors.ts +0 -245
- package/src/utils/fileImport.ts +0 -336
- package/src/utils/fileTreeConverter.ts +0 -160
- package/src/utils/filterResources.ts +0 -677
- package/src/utils/tsResIntegration.ts +0 -467
- package/src/utils/urlParams.ts +0 -238
- package/src/utils/zip/browserZipFileTreeAccessors.ts +0 -385
- package/tailwind.config.js +0 -21
- package/test-data/README.md +0 -184
- package/test-data/config-variations/README.md +0 -64
- package/test-data/config-variations/composition-terminology.json +0 -112
- package/test-data/config-variations/enterprise-complex.json +0 -129
- package/test-data/config-variations/gaming-app.json +0 -87
- package/test-data/config-variations/high-priority-language.json +0 -65
- package/test-data/config-variations/minimal-basic.json +0 -37
- package/test-data/config-variations/qualifier-default-values.json +0 -71
- package/test-data/config-variations/territory-first.json +0 -68
- package/test-data/config-variations/territory-hierarchy-test.json +0 -31
- package/test-data/config-variations/territory-hierarchy-unconstrained.json +0 -32
- package/test-data/default-matching-test/greeting.res.json +0 -83
- package/test-data/default-matching-test/only-defaults.res.json +0 -48
- package/test-data/sample-project/package.json +0 -27
- package/test-data/sample-project/src/resources/app-config.json +0 -27
- package/test-data/sample-project/src/resources/app.json +0 -177
- package/test-data/sample-project/src/resources/config/app.env=development.json +0 -21
- package/test-data/sample-project/src/resources/config/app.json +0 -21
- package/test-data/sample-project/src/resources/config/app.platform=mobile.json +0 -23
- package/test-data/sample-project/src/resources/feature-flags.json +0 -23
- package/test-data/sample-project/src/resources/home=CA/common.json +0 -16
- package/test-data/sample-project/src/resources/images/density=hdpi.json +0 -29
- package/test-data/sample-project/src/resources/images/home.json +0 -29
- package/test-data/sample-project/src/resources/images/logout.json +0 -14
- package/test-data/sample-project/src/resources/images/settings.json +0 -14
- package/test-data/sample-project/src/resources/images/ui-icons.json +0 -29
- package/test-data/sample-project/src/resources/images/user.density=hdpi.json +0 -14
- package/test-data/sample-project/src/resources/images/user.json +0 -14
- package/test-data/sample-project/src/resources/lang=en-US/common.json +0 -15
- package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +0 -19
- package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +0 -30
- package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +0 -25
- package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +0 -17
- package/test-data/sample-project/src/resources/region=europe/feature-flags.json +0 -17
- package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +0 -20
- package/test-data/sample-project/src/resources/strings/dashboard.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +0 -23
- package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +0 -21
- package/test-data/sample-project/src/resources/strings/language=en-US.json +0 -16
- package/test-data/sample-project/src/resources/strings.json +0 -78
- package/test-data/sample-project/src/resources/territory=BE/app-config.json +0 -19
- package/test-data/sample-project/src/resources/territory=CA/app-config.json +0 -21
- package/test-data/sample-project/src/resources/territory=GB/app-config.json +0 -25
- package/test-data/sample-project/src/resources/territory=NL/app-config.json +0 -25
- package/test-data/sample-project/src/resources/ui-terms.json +0 -96
- package/test-data/sample-project/src/resources-config.json +0 -111
- package/tsconfig.json +0 -27
- package/webpack.config.js +0 -60
|
@@ -1,1672 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
-
import { MagnifyingGlassIcon, CubeIcon, DocumentTextIcon } from '@heroicons/react/24/outline';
|
|
3
|
-
import { UseResourceManagerReturn } from '../../hooks/useResourceManager';
|
|
4
|
-
import { Message, FilterState } from '../../types/app';
|
|
5
|
-
import { FilterResult } from '../../utils/filterResources';
|
|
6
|
-
import { Runtime, Config, NoMatch } from '@fgv/ts-res';
|
|
7
|
-
import { createSimpleContext, DEFAULT_SYSTEM_CONFIGURATION } from '../../utils/tsResIntegration';
|
|
8
|
-
|
|
9
|
-
// Updated types for default resolution support
|
|
10
|
-
type ConditionMatchType = 'match' | 'matchAsDefault' | 'noMatch';
|
|
11
|
-
|
|
12
|
-
interface IConditionMatchResult {
|
|
13
|
-
score: number;
|
|
14
|
-
priority: number;
|
|
15
|
-
matchType: ConditionMatchType;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type DecisionResolutionResult =
|
|
19
|
-
| { success: false }
|
|
20
|
-
| { success: true; instanceIndices: ReadonlyArray<number>; defaultInstanceIndices: ReadonlyArray<number> };
|
|
21
|
-
|
|
22
|
-
interface ResolutionViewerProps {
|
|
23
|
-
onMessage?: (type: Message['type'], message: string) => void;
|
|
24
|
-
resourceManager: UseResourceManagerReturn;
|
|
25
|
-
filterState: FilterState;
|
|
26
|
-
filterResult?: FilterResult | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ContextState {
|
|
30
|
-
[qualifierName: string]: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface ConditionEvaluationResult {
|
|
34
|
-
qualifierName: string;
|
|
35
|
-
qualifierValue: any;
|
|
36
|
-
conditionValue: any;
|
|
37
|
-
operator: string;
|
|
38
|
-
score: number;
|
|
39
|
-
matched: boolean;
|
|
40
|
-
matchType: ConditionMatchType;
|
|
41
|
-
scoreAsDefault?: number;
|
|
42
|
-
conditionIndex: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface CandidateInfo {
|
|
46
|
-
candidate: Runtime.IResourceCandidate;
|
|
47
|
-
conditionSetKey: string | null;
|
|
48
|
-
candidateIndex: number;
|
|
49
|
-
matched: boolean;
|
|
50
|
-
matchType: ConditionMatchType;
|
|
51
|
-
isDefaultMatch: boolean;
|
|
52
|
-
conditionEvaluations?: ConditionEvaluationResult[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface ResolutionResult {
|
|
56
|
-
success: boolean;
|
|
57
|
-
resource?: Runtime.IResource;
|
|
58
|
-
bestCandidate?: Runtime.IResourceCandidate;
|
|
59
|
-
allCandidates?: readonly Runtime.IResourceCandidate[];
|
|
60
|
-
candidateDetails?: CandidateInfo[];
|
|
61
|
-
composedValue?: any;
|
|
62
|
-
error?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
type ViewMode = 'best' | 'all' | 'raw' | 'composed';
|
|
66
|
-
|
|
67
|
-
interface TooltipProps {
|
|
68
|
-
content: string;
|
|
69
|
-
children: React.ReactNode;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
|
|
73
|
-
const [isVisible, setIsVisible] = useState(false);
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div
|
|
77
|
-
className="relative inline-block"
|
|
78
|
-
onMouseEnter={() => setIsVisible(true)}
|
|
79
|
-
onMouseLeave={() => setIsVisible(false)}
|
|
80
|
-
>
|
|
81
|
-
{children}
|
|
82
|
-
{isVisible && (
|
|
83
|
-
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 z-10">
|
|
84
|
-
<div className="bg-gray-800 text-white text-xs rounded py-1 px-2 whitespace-nowrap">
|
|
85
|
-
{content}
|
|
86
|
-
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-800"></div>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
interface CacheDashboardProps {
|
|
95
|
-
cacheMetrics: Runtime.ResourceResolverCacheMetricsListener<Runtime.AggregateCacheMetrics> | null;
|
|
96
|
-
resolver: Runtime.ResourceResolver | null;
|
|
97
|
-
onReset: () => void;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const CacheDashboard: React.FC<CacheDashboardProps> = ({ cacheMetrics, resolver, onReset }) => {
|
|
101
|
-
if (!cacheMetrics || !resolver) {
|
|
102
|
-
return (
|
|
103
|
-
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
|
|
104
|
-
<div className="flex items-center justify-between">
|
|
105
|
-
<span className="text-sm text-gray-500">📊 Cache: Not active</span>
|
|
106
|
-
<button
|
|
107
|
-
onClick={onReset}
|
|
108
|
-
disabled={true}
|
|
109
|
-
className="text-xs px-2 py-1 bg-gray-200 text-gray-400 rounded cursor-not-allowed"
|
|
110
|
-
>
|
|
111
|
-
Reset
|
|
112
|
-
</button>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const metrics = cacheMetrics.metrics;
|
|
119
|
-
|
|
120
|
-
// Helper function to get cache display info
|
|
121
|
-
const getCacheDisplay = (cacheType: Runtime.ResourceResolverCacheType) => {
|
|
122
|
-
const cacheMetric = metrics[cacheType];
|
|
123
|
-
const hitRate = cacheMetric.hitRate;
|
|
124
|
-
const totalAccesses = cacheMetric.totalAccesses;
|
|
125
|
-
const hits = cacheMetric.hits;
|
|
126
|
-
const misses = cacheMetric.misses;
|
|
127
|
-
const errors = cacheMetric.errors;
|
|
128
|
-
|
|
129
|
-
// Get actual filled cache slots by looking at the cache arrays
|
|
130
|
-
const cacheArray =
|
|
131
|
-
cacheType === 'condition'
|
|
132
|
-
? resolver.conditionCache
|
|
133
|
-
: cacheType === 'conditionSet'
|
|
134
|
-
? resolver.conditionSetCache
|
|
135
|
-
: resolver.decisionCache;
|
|
136
|
-
const filledSlots = cacheArray.filter((slot) => slot !== undefined).length;
|
|
137
|
-
|
|
138
|
-
const totalSlots =
|
|
139
|
-
cacheType === 'condition'
|
|
140
|
-
? resolver.conditionCacheSize
|
|
141
|
-
: cacheType === 'conditionSet'
|
|
142
|
-
? resolver.conditionSetCacheSize
|
|
143
|
-
: resolver.decisionCacheSize;
|
|
144
|
-
|
|
145
|
-
// Color based on hit rate
|
|
146
|
-
const getColor = (rate: number) => {
|
|
147
|
-
if (rate === 0) return 'text-gray-500';
|
|
148
|
-
if (rate >= 80) return 'text-green-600';
|
|
149
|
-
if (rate >= 50) return 'text-yellow-600';
|
|
150
|
-
return 'text-red-600';
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
filledSlots,
|
|
155
|
-
totalSlots,
|
|
156
|
-
totalAccesses,
|
|
157
|
-
hits,
|
|
158
|
-
misses,
|
|
159
|
-
errors,
|
|
160
|
-
hitRate: Math.round(hitRate),
|
|
161
|
-
color: getColor(hitRate)
|
|
162
|
-
};
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const conditionDisplay = getCacheDisplay('condition');
|
|
166
|
-
const conditionSetDisplay = getCacheDisplay('conditionSet');
|
|
167
|
-
const decisionDisplay = getCacheDisplay('decision');
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
|
|
171
|
-
<div className="flex items-center justify-between">
|
|
172
|
-
<div className="flex items-center space-x-4 text-sm">
|
|
173
|
-
<span className="text-gray-700">📊 Cache:</span>
|
|
174
|
-
<Tooltip
|
|
175
|
-
content={`Condition Cache: ${conditionDisplay.filledSlots} filled out of ${conditionDisplay.totalSlots} slots, ${conditionDisplay.totalAccesses} total accesses (${conditionDisplay.hits} hits, ${conditionDisplay.misses} misses, ${conditionDisplay.errors} errors), ${conditionDisplay.hitRate}% hit rate`}
|
|
176
|
-
>
|
|
177
|
-
<span className={`font-mono ${conditionDisplay.color} cursor-help`}>
|
|
178
|
-
Cond: {conditionDisplay.filledSlots}/{conditionDisplay.totalSlots} ({conditionDisplay.hitRate}%)
|
|
179
|
-
</span>
|
|
180
|
-
</Tooltip>
|
|
181
|
-
<span className="text-gray-400">|</span>
|
|
182
|
-
<Tooltip
|
|
183
|
-
content={`ConditionSet Cache: ${conditionSetDisplay.filledSlots} filled out of ${conditionSetDisplay.totalSlots} slots, ${conditionSetDisplay.totalAccesses} total accesses (${conditionSetDisplay.hits} hits, ${conditionSetDisplay.misses} misses, ${conditionSetDisplay.errors} errors), ${conditionSetDisplay.hitRate}% hit rate`}
|
|
184
|
-
>
|
|
185
|
-
<span className={`font-mono ${conditionSetDisplay.color} cursor-help`}>
|
|
186
|
-
CondSet: {conditionSetDisplay.filledSlots}/{conditionSetDisplay.totalSlots} (
|
|
187
|
-
{conditionSetDisplay.hitRate}%)
|
|
188
|
-
</span>
|
|
189
|
-
</Tooltip>
|
|
190
|
-
<span className="text-gray-400">|</span>
|
|
191
|
-
<Tooltip
|
|
192
|
-
content={`Decision Cache: ${decisionDisplay.filledSlots} filled out of ${decisionDisplay.totalSlots} slots, ${decisionDisplay.totalAccesses} total accesses (${decisionDisplay.hits} hits, ${decisionDisplay.misses} misses, ${decisionDisplay.errors} errors), ${decisionDisplay.hitRate}% hit rate`}
|
|
193
|
-
>
|
|
194
|
-
<span className={`font-mono ${decisionDisplay.color} cursor-help`}>
|
|
195
|
-
Dec: {decisionDisplay.filledSlots}/{decisionDisplay.totalSlots} ({decisionDisplay.hitRate}%)
|
|
196
|
-
</span>
|
|
197
|
-
</Tooltip>
|
|
198
|
-
</div>
|
|
199
|
-
<Tooltip content="Clear all caches and reset metrics">
|
|
200
|
-
<button
|
|
201
|
-
onClick={onReset}
|
|
202
|
-
className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
|
|
203
|
-
>
|
|
204
|
-
Reset
|
|
205
|
-
</button>
|
|
206
|
-
</Tooltip>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
);
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// Component to display condition evaluation results
|
|
213
|
-
interface ConditionEvaluationDisplayProps {
|
|
214
|
-
evaluations: ConditionEvaluationResult[];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const ConditionEvaluationDisplay: React.FC<ConditionEvaluationDisplayProps> = ({ evaluations }) => {
|
|
218
|
-
if (!evaluations || evaluations.length === 0) {
|
|
219
|
-
return <span className="text-xs text-gray-500">No conditions</span>;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const getMatchTypeColor = (type: ConditionMatchType) => {
|
|
223
|
-
switch (type) {
|
|
224
|
-
case 'match':
|
|
225
|
-
return 'bg-green-100 text-green-800';
|
|
226
|
-
case 'matchAsDefault':
|
|
227
|
-
return 'bg-amber-100 text-amber-800';
|
|
228
|
-
case 'noMatch':
|
|
229
|
-
return 'bg-red-100 text-red-800';
|
|
230
|
-
default:
|
|
231
|
-
return 'bg-gray-100 text-gray-800';
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const getMatchTypeIcon = (type: ConditionMatchType) => {
|
|
236
|
-
switch (type) {
|
|
237
|
-
case 'match':
|
|
238
|
-
return '✓';
|
|
239
|
-
case 'matchAsDefault':
|
|
240
|
-
return '≈';
|
|
241
|
-
case 'noMatch':
|
|
242
|
-
return '✗';
|
|
243
|
-
default:
|
|
244
|
-
return '?';
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
<div className="flex flex-wrap gap-1 mt-1">
|
|
250
|
-
{evaluations.map((evaluation, index) => {
|
|
251
|
-
const displayText = `${evaluation.qualifierName}=${
|
|
252
|
-
evaluation.conditionValue
|
|
253
|
-
} (${evaluation.score.toFixed(1)})`;
|
|
254
|
-
|
|
255
|
-
const tooltipContent = [
|
|
256
|
-
`${evaluation.qualifierName}: context="${evaluation.qualifierValue}" ${evaluation.operator} condition="${evaluation.conditionValue}"`,
|
|
257
|
-
`→ score=${evaluation.score} (${evaluation.matchType})`,
|
|
258
|
-
evaluation.scoreAsDefault !== undefined ? `default score: ${evaluation.scoreAsDefault}` : null,
|
|
259
|
-
`[condition index: ${evaluation.conditionIndex}]`
|
|
260
|
-
]
|
|
261
|
-
.filter(Boolean)
|
|
262
|
-
.join(' ');
|
|
263
|
-
|
|
264
|
-
return (
|
|
265
|
-
<Tooltip key={index} content={tooltipContent}>
|
|
266
|
-
<span className={`text-xs px-2 py-1 rounded ${getMatchTypeColor(evaluation.matchType)}`}>
|
|
267
|
-
{getMatchTypeIcon(evaluation.matchType)} {displayText}
|
|
268
|
-
</span>
|
|
269
|
-
</Tooltip>
|
|
270
|
-
);
|
|
271
|
-
})}
|
|
272
|
-
</div>
|
|
273
|
-
);
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
// Cache Contents Display Component
|
|
277
|
-
interface CacheContentsDisplayProps {
|
|
278
|
-
resolver: Runtime.ResourceResolver | null;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const CacheContentsDisplay: React.FC<CacheContentsDisplayProps> = ({ resolver }) => {
|
|
282
|
-
const [expanded, setExpanded] = useState(false);
|
|
283
|
-
|
|
284
|
-
if (!resolver) {
|
|
285
|
-
return (
|
|
286
|
-
<div className="border border-gray-200 rounded-lg bg-gray-50 p-3">
|
|
287
|
-
<div className="flex items-center justify-between">
|
|
288
|
-
<span className="text-sm text-gray-500">🗄️ Cache Contents</span>
|
|
289
|
-
<span className="text-xs text-gray-400">Not available</span>
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const conditionCache = resolver.conditionCache;
|
|
296
|
-
const conditionSetCache = resolver.conditionSetCache;
|
|
297
|
-
const decisionCache = resolver.decisionCache;
|
|
298
|
-
|
|
299
|
-
const conditionHits = conditionCache.filter((c) => c !== undefined).length;
|
|
300
|
-
const conditionSetHits = conditionSetCache.filter((cs) => cs !== undefined).length;
|
|
301
|
-
const decisionHits = decisionCache.filter((d) => d !== undefined).length;
|
|
302
|
-
|
|
303
|
-
const totalCacheEntries = conditionHits + conditionSetHits + decisionHits;
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<div className="border border-gray-200 rounded-lg bg-gray-50">
|
|
307
|
-
<div
|
|
308
|
-
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100"
|
|
309
|
-
onClick={() => setExpanded(!expanded)}
|
|
310
|
-
>
|
|
311
|
-
<div className="flex items-center space-x-2">
|
|
312
|
-
<span className="text-sm text-gray-700">🗄️ Cache Contents</span>
|
|
313
|
-
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
314
|
-
{totalCacheEntries} cached
|
|
315
|
-
</span>
|
|
316
|
-
</div>
|
|
317
|
-
<div className="flex items-center space-x-2">
|
|
318
|
-
<span className="text-xs text-gray-500">
|
|
319
|
-
C:{conditionHits} CS:{conditionSetHits} D:{decisionHits}
|
|
320
|
-
</span>
|
|
321
|
-
<span className="text-xs text-gray-400">{expanded ? '▼' : '▶'}</span>
|
|
322
|
-
</div>
|
|
323
|
-
</div>
|
|
324
|
-
|
|
325
|
-
{expanded && (
|
|
326
|
-
<div className="border-t border-gray-200 p-3 space-y-3 max-h-64 overflow-y-auto">
|
|
327
|
-
{/* Condition Cache */}
|
|
328
|
-
{conditionHits > 0 && (
|
|
329
|
-
<div>
|
|
330
|
-
<h4 className="text-xs font-medium text-gray-700 mb-2">Conditions ({conditionHits})</h4>
|
|
331
|
-
<div className="space-y-1">
|
|
332
|
-
{conditionCache.map((score, index) => {
|
|
333
|
-
if (score === undefined) return null;
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const condition = resolver.resourceManager.conditions.getAt(index);
|
|
337
|
-
if (condition.isFailure()) return null;
|
|
338
|
-
|
|
339
|
-
const conditionObj = condition.value;
|
|
340
|
-
const qualifierName = conditionObj.qualifier.name;
|
|
341
|
-
|
|
342
|
-
// Determine match type from cached result
|
|
343
|
-
const cachedResult = score as unknown as IConditionMatchResult;
|
|
344
|
-
const matchType =
|
|
345
|
-
cachedResult?.matchType || (cachedResult?.score > 0 ? 'match' : 'noMatch');
|
|
346
|
-
const displayScore = cachedResult?.score || 0;
|
|
347
|
-
|
|
348
|
-
const getMatchTypeColor = (type: ConditionMatchType) => {
|
|
349
|
-
switch (type) {
|
|
350
|
-
case 'match':
|
|
351
|
-
return 'bg-green-100 text-green-800';
|
|
352
|
-
case 'matchAsDefault':
|
|
353
|
-
return 'bg-amber-100 text-amber-800';
|
|
354
|
-
case 'noMatch':
|
|
355
|
-
return 'bg-red-100 text-red-800';
|
|
356
|
-
default:
|
|
357
|
-
return 'bg-gray-100 text-gray-800';
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const getMatchTypeIcon = (type: ConditionMatchType) => {
|
|
362
|
-
switch (type) {
|
|
363
|
-
case 'match':
|
|
364
|
-
return '✓';
|
|
365
|
-
case 'matchAsDefault':
|
|
366
|
-
return '≈';
|
|
367
|
-
case 'noMatch':
|
|
368
|
-
return '✗';
|
|
369
|
-
default:
|
|
370
|
-
return '?';
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
return (
|
|
375
|
-
<div key={index} className="flex items-center justify-between text-xs">
|
|
376
|
-
<div className="text-gray-600 truncate max-w-24">
|
|
377
|
-
<div>
|
|
378
|
-
{index}: {qualifierName}={JSON.stringify(conditionObj.value)}
|
|
379
|
-
</div>
|
|
380
|
-
{conditionObj.scoreAsDefault !== undefined && (
|
|
381
|
-
<div className="text-amber-600 font-medium text-xs">
|
|
382
|
-
default: {conditionObj.scoreAsDefault}
|
|
383
|
-
</div>
|
|
384
|
-
)}
|
|
385
|
-
</div>
|
|
386
|
-
<div className="flex items-center space-x-1">
|
|
387
|
-
<span className={`px-2 py-1 rounded ${getMatchTypeColor(matchType)}`}>
|
|
388
|
-
{getMatchTypeIcon(matchType)} {matchType}
|
|
389
|
-
</span>
|
|
390
|
-
<span className="text-xs text-gray-500">({displayScore.toFixed(2)})</span>
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
);
|
|
394
|
-
} catch (error) {
|
|
395
|
-
return (
|
|
396
|
-
<div key={index} className="flex items-center justify-between text-xs">
|
|
397
|
-
<span className="text-gray-400">
|
|
398
|
-
{index}: condition-{index}
|
|
399
|
-
</span>
|
|
400
|
-
<span className="bg-gray-100 text-gray-600 px-2 py-1 rounded">
|
|
401
|
-
condition-{index} ({score.toFixed(2)})
|
|
402
|
-
</span>
|
|
403
|
-
</div>
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
})}
|
|
407
|
-
</div>
|
|
408
|
-
</div>
|
|
409
|
-
)}
|
|
410
|
-
|
|
411
|
-
{/* Condition Set Cache */}
|
|
412
|
-
{conditionSetHits > 0 && (
|
|
413
|
-
<div>
|
|
414
|
-
<h4 className="text-xs font-medium text-gray-700 mb-2">Condition Sets ({conditionSetHits})</h4>
|
|
415
|
-
<div className="space-y-1">
|
|
416
|
-
{conditionSetCache.map((result, index) => {
|
|
417
|
-
if (result === undefined) return null;
|
|
418
|
-
|
|
419
|
-
try {
|
|
420
|
-
const conditionSet = resolver.resourceManager.conditionSets.getAt(index);
|
|
421
|
-
const key = conditionSet.isSuccess() ? conditionSet.value.key : `cs-${index}`;
|
|
422
|
-
|
|
423
|
-
return (
|
|
424
|
-
<div key={index} className="flex items-center justify-between text-xs">
|
|
425
|
-
<span className="text-gray-600 truncate max-w-24">
|
|
426
|
-
{index}: {key}
|
|
427
|
-
</span>
|
|
428
|
-
<span
|
|
429
|
-
className={`px-2 py-1 rounded ${
|
|
430
|
-
result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
431
|
-
}`}
|
|
432
|
-
>
|
|
433
|
-
{key} ({result.success ? '✓' : '✗'})
|
|
434
|
-
</span>
|
|
435
|
-
</div>
|
|
436
|
-
);
|
|
437
|
-
} catch (error) {
|
|
438
|
-
return (
|
|
439
|
-
<div key={index} className="flex items-center justify-between text-xs">
|
|
440
|
-
<span className="text-gray-400">
|
|
441
|
-
{index}: cs-{index}
|
|
442
|
-
</span>
|
|
443
|
-
<span
|
|
444
|
-
className={`px-2 py-1 rounded ${
|
|
445
|
-
result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
446
|
-
}`}
|
|
447
|
-
>
|
|
448
|
-
cs-{index} ({result.success ? '✓' : '✗'})
|
|
449
|
-
</span>
|
|
450
|
-
</div>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
})}
|
|
454
|
-
</div>
|
|
455
|
-
</div>
|
|
456
|
-
)}
|
|
457
|
-
|
|
458
|
-
{/* Decision Cache */}
|
|
459
|
-
{decisionHits > 0 && (
|
|
460
|
-
<div>
|
|
461
|
-
<h4 className="text-xs font-medium text-gray-700 mb-2">Decisions ({decisionHits})</h4>
|
|
462
|
-
<div className="space-y-1">
|
|
463
|
-
{decisionCache.map((result, index) => {
|
|
464
|
-
if (result === undefined) return null;
|
|
465
|
-
|
|
466
|
-
try {
|
|
467
|
-
const decision = resolver.resourceManager.decisions.getAt(index);
|
|
468
|
-
const key = decision.isSuccess() ? decision.value.key : `decision-${index}`;
|
|
469
|
-
const regularMatches = result.success ? result.instanceIndices.length : 0;
|
|
470
|
-
const defaultMatches = result.success ? result.defaultInstanceIndices.length : 0;
|
|
471
|
-
|
|
472
|
-
return (
|
|
473
|
-
<div key={index} className="flex items-center justify-between text-xs">
|
|
474
|
-
<span className="text-gray-600 truncate max-w-24">
|
|
475
|
-
{index}: {key}
|
|
476
|
-
</span>
|
|
477
|
-
<div className="flex items-center space-x-1">
|
|
478
|
-
<span
|
|
479
|
-
className={`px-2 py-1 rounded ${
|
|
480
|
-
result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
481
|
-
}`}
|
|
482
|
-
>
|
|
483
|
-
{regularMatches} regular
|
|
484
|
-
</span>
|
|
485
|
-
{defaultMatches > 0 && (
|
|
486
|
-
<span className="px-2 py-1 rounded bg-amber-100 text-amber-800">
|
|
487
|
-
{defaultMatches} default
|
|
488
|
-
</span>
|
|
489
|
-
)}
|
|
490
|
-
</div>
|
|
491
|
-
</div>
|
|
492
|
-
);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
return (
|
|
495
|
-
<div key={index} className="flex items-center justify-between text-xs">
|
|
496
|
-
<span className="text-gray-400">
|
|
497
|
-
{index}: decision-{index}
|
|
498
|
-
</span>
|
|
499
|
-
<span
|
|
500
|
-
className={`px-2 py-1 rounded ${
|
|
501
|
-
result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
502
|
-
}`}
|
|
503
|
-
>
|
|
504
|
-
decision-{index} ({result.success ? result.instanceIndices.length : '0'})
|
|
505
|
-
</span>
|
|
506
|
-
</div>
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
})}
|
|
510
|
-
</div>
|
|
511
|
-
</div>
|
|
512
|
-
)}
|
|
513
|
-
|
|
514
|
-
{totalCacheEntries === 0 && (
|
|
515
|
-
<div className="text-center text-xs text-gray-500 py-4">
|
|
516
|
-
No cache entries. Resolve a resource to populate the cache.
|
|
517
|
-
</div>
|
|
518
|
-
)}
|
|
519
|
-
</div>
|
|
520
|
-
)}
|
|
521
|
-
</div>
|
|
522
|
-
);
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
const ResolutionViewer: React.FC<ResolutionViewerProps> = ({
|
|
526
|
-
onMessage,
|
|
527
|
-
resourceManager,
|
|
528
|
-
filterState,
|
|
529
|
-
filterResult
|
|
530
|
-
}) => {
|
|
531
|
-
const { state: resourceState } = resourceManager;
|
|
532
|
-
|
|
533
|
-
// Use filtered resources when filtering is active and successful
|
|
534
|
-
const isFilteringActive = filterState.enabled && filterResult?.success === true;
|
|
535
|
-
const activeProcessedResources = isFilteringActive
|
|
536
|
-
? filterResult?.processedResources
|
|
537
|
-
: resourceState.processedResources;
|
|
538
|
-
|
|
539
|
-
// Available qualifiers
|
|
540
|
-
const availableQualifiers = useMemo(() => {
|
|
541
|
-
if (activeProcessedResources?.compiledCollection.qualifiers) {
|
|
542
|
-
// Get qualifier names from the compiled collection
|
|
543
|
-
return activeProcessedResources.compiledCollection.qualifiers.map((q) => q.name);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Use active configuration if available, otherwise fall back to default
|
|
547
|
-
const config = resourceState.activeConfiguration || DEFAULT_SYSTEM_CONFIGURATION;
|
|
548
|
-
return config.qualifiers.map((q) => q.name);
|
|
549
|
-
}, [activeProcessedResources?.compiledCollection.qualifiers, resourceState.activeConfiguration]);
|
|
550
|
-
|
|
551
|
-
// Get qualifier type information for UI decisions
|
|
552
|
-
const qualifierTypeInfo = useMemo(() => {
|
|
553
|
-
const info: Record<
|
|
554
|
-
string,
|
|
555
|
-
{
|
|
556
|
-
type: Config.Model.ISystemConfiguration['qualifierTypes'][0];
|
|
557
|
-
enumeratedValues?: string[];
|
|
558
|
-
}
|
|
559
|
-
> = {};
|
|
560
|
-
|
|
561
|
-
// Get the active configuration to access qualifier types
|
|
562
|
-
const config = resourceState.activeConfiguration || DEFAULT_SYSTEM_CONFIGURATION;
|
|
563
|
-
|
|
564
|
-
// Create a map of qualifier name to qualifier type for quick lookup
|
|
565
|
-
const qualifierTypeMap: Record<string, Config.Model.ISystemConfiguration['qualifierTypes'][0]> = {};
|
|
566
|
-
|
|
567
|
-
// First, create a mapping of qualifiers to their types
|
|
568
|
-
config.qualifiers.forEach((qualifier) => {
|
|
569
|
-
const qualifierType = config.qualifierTypes.find((qt) => qt.name === qualifier.typeName);
|
|
570
|
-
if (qualifierType) {
|
|
571
|
-
qualifierTypeMap[qualifier.name] = qualifierType;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
// Build info for each qualifier
|
|
576
|
-
availableQualifiers.forEach((qualifierName) => {
|
|
577
|
-
const qualifierType = qualifierTypeMap[qualifierName];
|
|
578
|
-
if (qualifierType) {
|
|
579
|
-
info[qualifierName] = { type: qualifierType };
|
|
580
|
-
|
|
581
|
-
// Extract enumerated values for literal types
|
|
582
|
-
if (
|
|
583
|
-
qualifierType.systemType === 'literal' &&
|
|
584
|
-
qualifierType.configuration &&
|
|
585
|
-
typeof qualifierType.configuration === 'object' &&
|
|
586
|
-
'enumeratedValues' in qualifierType.configuration &&
|
|
587
|
-
Array.isArray(qualifierType.configuration.enumeratedValues)
|
|
588
|
-
) {
|
|
589
|
-
info[qualifierName].enumeratedValues = qualifierType.configuration.enumeratedValues;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
return info;
|
|
595
|
-
}, [availableQualifiers, resourceState.activeConfiguration]);
|
|
596
|
-
|
|
597
|
-
// Initialize context with smart default values based on qualifier types
|
|
598
|
-
const defaultContextValues = useMemo(() => {
|
|
599
|
-
const defaults: ContextState = {};
|
|
600
|
-
|
|
601
|
-
// Compute intelligent defaults based on qualifier types
|
|
602
|
-
availableQualifiers.forEach((qualifierName) => {
|
|
603
|
-
const typeInfo = qualifierTypeInfo[qualifierName];
|
|
604
|
-
|
|
605
|
-
if (!typeInfo) {
|
|
606
|
-
// No type info available, use empty string
|
|
607
|
-
defaults[qualifierName] = '';
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const qualifierType = typeInfo.type;
|
|
612
|
-
|
|
613
|
-
// Compute default based on system type
|
|
614
|
-
switch (qualifierType.systemType) {
|
|
615
|
-
case 'language':
|
|
616
|
-
// Language qualifiers default to en-US
|
|
617
|
-
defaults[qualifierName] = 'en-US';
|
|
618
|
-
break;
|
|
619
|
-
|
|
620
|
-
case 'territory':
|
|
621
|
-
// Territory qualifiers: use first allowed territory if constrained, otherwise US
|
|
622
|
-
if (
|
|
623
|
-
qualifierType.configuration &&
|
|
624
|
-
typeof qualifierType.configuration === 'object' &&
|
|
625
|
-
'allowedTerritories' in qualifierType.configuration &&
|
|
626
|
-
Array.isArray(qualifierType.configuration.allowedTerritories) &&
|
|
627
|
-
qualifierType.configuration.allowedTerritories.length > 0
|
|
628
|
-
) {
|
|
629
|
-
defaults[qualifierName] = qualifierType.configuration.allowedTerritories[0];
|
|
630
|
-
} else {
|
|
631
|
-
defaults[qualifierName] = 'US';
|
|
632
|
-
}
|
|
633
|
-
break;
|
|
634
|
-
|
|
635
|
-
case 'literal':
|
|
636
|
-
// Literal qualifiers: use first enumerated value if constrained, otherwise "unknown"
|
|
637
|
-
if (typeInfo.enumeratedValues && typeInfo.enumeratedValues.length > 0) {
|
|
638
|
-
defaults[qualifierName] = typeInfo.enumeratedValues[0];
|
|
639
|
-
} else {
|
|
640
|
-
defaults[qualifierName] = 'unknown';
|
|
641
|
-
}
|
|
642
|
-
break;
|
|
643
|
-
|
|
644
|
-
default:
|
|
645
|
-
defaults[qualifierName] = '';
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
return defaults;
|
|
650
|
-
}, [availableQualifiers, qualifierTypeInfo]);
|
|
651
|
-
|
|
652
|
-
// Context state
|
|
653
|
-
const [contextValues, setContextValues] = useState<ContextState>({});
|
|
654
|
-
const [pendingContextValues, setPendingContextValues] = useState<ContextState>({});
|
|
655
|
-
|
|
656
|
-
// Update context state when default values change
|
|
657
|
-
React.useEffect(() => {
|
|
658
|
-
setContextValues(defaultContextValues);
|
|
659
|
-
setPendingContextValues(defaultContextValues);
|
|
660
|
-
}, [defaultContextValues]);
|
|
661
|
-
|
|
662
|
-
// Selection state
|
|
663
|
-
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
|
|
664
|
-
const [viewMode, setViewMode] = useState<ViewMode>('best');
|
|
665
|
-
|
|
666
|
-
// Resolution state
|
|
667
|
-
const [currentResolver, setCurrentResolver] = useState<Runtime.ResourceResolver | null>(null);
|
|
668
|
-
const [resolutionResult, setResolutionResult] = useState<ResolutionResult | null>(null);
|
|
669
|
-
|
|
670
|
-
// Track if we've auto-applied defaults for the current configuration
|
|
671
|
-
const [hasAutoApplied, setHasAutoApplied] = useState(false);
|
|
672
|
-
|
|
673
|
-
// Cache metrics state
|
|
674
|
-
const [cacheMetrics, setCacheMetrics] =
|
|
675
|
-
useState<Runtime.ResourceResolverCacheMetricsListener<Runtime.AggregateCacheMetrics> | null>(null);
|
|
676
|
-
|
|
677
|
-
// Available resources
|
|
678
|
-
const availableResources = useMemo(() => {
|
|
679
|
-
if (!activeProcessedResources?.summary?.resourceIds) {
|
|
680
|
-
return [];
|
|
681
|
-
}
|
|
682
|
-
return activeProcessedResources.summary.resourceIds.sort();
|
|
683
|
-
}, [activeProcessedResources?.summary?.resourceIds]);
|
|
684
|
-
|
|
685
|
-
// Check if there are pending context changes
|
|
686
|
-
const hasPendingChanges = useMemo(() => {
|
|
687
|
-
return JSON.stringify(contextValues) !== JSON.stringify(pendingContextValues);
|
|
688
|
-
}, [contextValues, pendingContextValues]);
|
|
689
|
-
|
|
690
|
-
// Helper functions for condition set keys (similar to CompiledBrowser)
|
|
691
|
-
const getConditionKey = useCallback((condition: any, compiledCollection: any): string => {
|
|
692
|
-
try {
|
|
693
|
-
// Use metadata if available
|
|
694
|
-
if (condition.metadata?.key) {
|
|
695
|
-
return condition.metadata.key;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Fall back to manual construction
|
|
699
|
-
const qualifier = compiledCollection.qualifiers[condition.qualifierIndex];
|
|
700
|
-
if (!qualifier) return `unknown-qualifier`;
|
|
701
|
-
|
|
702
|
-
// Create a meaningful key like "language=en-US" or "territory=US"
|
|
703
|
-
const key = `${qualifier.name}=${condition.value}`;
|
|
704
|
-
return key;
|
|
705
|
-
} catch (error) {
|
|
706
|
-
return `condition-${condition.qualifierIndex}`;
|
|
707
|
-
}
|
|
708
|
-
}, []);
|
|
709
|
-
|
|
710
|
-
const getConditionSetKey = useCallback(
|
|
711
|
-
(conditionSet: any, conditionSetIndex: number, compiledCollection: any): string => {
|
|
712
|
-
try {
|
|
713
|
-
// Use metadata if available
|
|
714
|
-
if (conditionSet.metadata?.key) {
|
|
715
|
-
return conditionSet.metadata.key;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (conditionSetIndex === 0) {
|
|
719
|
-
return 'unconditional';
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Fall back to manual construction
|
|
723
|
-
if (!conditionSet.conditions || conditionSet.conditions.length === 0) {
|
|
724
|
-
return `condition-set-${conditionSetIndex}`;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Build a composite key from all conditions in the set
|
|
728
|
-
const conditionKeys = conditionSet.conditions.map((conditionIndex: number) => {
|
|
729
|
-
const condition = compiledCollection.conditions[conditionIndex];
|
|
730
|
-
if (!condition) return `unknown-${conditionIndex}`;
|
|
731
|
-
return getConditionKey(condition, compiledCollection);
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
return conditionKeys.join(',');
|
|
735
|
-
} catch (error) {
|
|
736
|
-
return `condition-set-${conditionSetIndex}`;
|
|
737
|
-
}
|
|
738
|
-
},
|
|
739
|
-
[getConditionKey]
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
const getCandidateConditionSetKey = useCallback(
|
|
743
|
-
(candidateIndex: number, resource: any, compiledCollection: any): string | null => {
|
|
744
|
-
try {
|
|
745
|
-
// Get the decision for this resource
|
|
746
|
-
const decision = compiledCollection.decisions[resource.decision];
|
|
747
|
-
if (!decision || !decision.conditionSets) {
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Map candidate index to condition set index
|
|
752
|
-
// Assuming candidates correspond to condition sets in order
|
|
753
|
-
if (candidateIndex >= decision.conditionSets.length) {
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const conditionSetIndex = decision.conditionSets[candidateIndex];
|
|
758
|
-
const conditionSet = compiledCollection.conditionSets[conditionSetIndex];
|
|
759
|
-
if (!conditionSet) {
|
|
760
|
-
return null;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
return getConditionSetKey(conditionSet, conditionSetIndex, compiledCollection);
|
|
764
|
-
} catch (error) {
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
},
|
|
768
|
-
[getConditionSetKey]
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
// Apply context changes
|
|
772
|
-
const applyContext = useCallback(() => {
|
|
773
|
-
if (!activeProcessedResources?.system) {
|
|
774
|
-
onMessage?.('error', 'No resources loaded');
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Create new context provider
|
|
779
|
-
const contextResult = createSimpleContext(pendingContextValues, activeProcessedResources.system);
|
|
780
|
-
if (contextResult.isFailure()) {
|
|
781
|
-
onMessage?.('error', `Failed to create context: ${contextResult.message}`);
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Reset existing cache and metrics when context changes
|
|
786
|
-
if (currentResolver && cacheMetrics) {
|
|
787
|
-
currentResolver.clearConditionCache();
|
|
788
|
-
cacheMetrics.reset();
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Create cache metrics listener
|
|
792
|
-
const metricsListener = new Runtime.ResourceResolverCacheMetricsListener(
|
|
793
|
-
() => new Runtime.AggregateCacheMetrics()
|
|
794
|
-
);
|
|
795
|
-
setCacheMetrics(metricsListener);
|
|
796
|
-
|
|
797
|
-
// Create new resource resolver
|
|
798
|
-
const resolverResult = Runtime.ResourceResolver.create({
|
|
799
|
-
resourceManager: activeProcessedResources.system.resourceManager,
|
|
800
|
-
qualifierTypes: activeProcessedResources.system.qualifierTypes,
|
|
801
|
-
contextQualifierProvider: contextResult.value,
|
|
802
|
-
listener: metricsListener
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
if (resolverResult.isFailure()) {
|
|
806
|
-
onMessage?.('error', `Failed to create resolver: ${resolverResult.message}`);
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Update state
|
|
811
|
-
setContextValues({ ...pendingContextValues });
|
|
812
|
-
setCurrentResolver(resolverResult.value);
|
|
813
|
-
|
|
814
|
-
// If a resource is selected, resolve it with the new context
|
|
815
|
-
if (selectedResourceId) {
|
|
816
|
-
resolveSelectedResource(resolverResult.value, selectedResourceId);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
onMessage?.('success', 'Context applied successfully');
|
|
820
|
-
}, [pendingContextValues, activeProcessedResources?.system, selectedResourceId, onMessage]);
|
|
821
|
-
|
|
822
|
-
// Helper function to evaluate conditions for a candidate
|
|
823
|
-
const evaluateConditionsForCandidate = useCallback(
|
|
824
|
-
(
|
|
825
|
-
resolver: Runtime.ResourceResolver,
|
|
826
|
-
candidateIndex: number,
|
|
827
|
-
compiledResource: any,
|
|
828
|
-
compiledCollection: any
|
|
829
|
-
): ConditionEvaluationResult[] => {
|
|
830
|
-
try {
|
|
831
|
-
const decision = compiledCollection.decisions[compiledResource.decision];
|
|
832
|
-
if (!decision || !decision.conditionSets || candidateIndex >= decision.conditionSets.length) {
|
|
833
|
-
return [];
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const conditionSetIndex = decision.conditionSets[candidateIndex];
|
|
837
|
-
const conditionSet = compiledCollection.conditionSets[conditionSetIndex];
|
|
838
|
-
if (!conditionSet || !conditionSet.conditions) {
|
|
839
|
-
return [];
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const evaluations: ConditionEvaluationResult[] = [];
|
|
843
|
-
|
|
844
|
-
for (const conditionIndex of conditionSet.conditions) {
|
|
845
|
-
const condition = compiledCollection.conditions[conditionIndex];
|
|
846
|
-
if (!condition) continue;
|
|
847
|
-
|
|
848
|
-
const qualifier = compiledCollection.qualifiers[condition.qualifierIndex];
|
|
849
|
-
if (!qualifier) continue;
|
|
850
|
-
|
|
851
|
-
// Get the qualifier value from context
|
|
852
|
-
const qualifierValueResult = resolver.contextQualifierProvider.get(qualifier);
|
|
853
|
-
const qualifierValue = qualifierValueResult.isSuccess() ? qualifierValueResult.value : null;
|
|
854
|
-
|
|
855
|
-
// Get the cached condition result from resolver
|
|
856
|
-
const cachedResult = resolver.conditionCache[conditionIndex] as IConditionMatchResult;
|
|
857
|
-
const score = cachedResult?.score || 0;
|
|
858
|
-
const matchType = cachedResult?.matchType || 'noMatch';
|
|
859
|
-
const matched = matchType !== 'noMatch';
|
|
860
|
-
|
|
861
|
-
evaluations.push({
|
|
862
|
-
qualifierName: qualifier.name,
|
|
863
|
-
qualifierValue,
|
|
864
|
-
conditionValue: condition.value,
|
|
865
|
-
operator: condition.operator || 'eq',
|
|
866
|
-
score,
|
|
867
|
-
matched,
|
|
868
|
-
matchType,
|
|
869
|
-
scoreAsDefault: condition.scoreAsDefault,
|
|
870
|
-
conditionIndex
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
return evaluations;
|
|
875
|
-
} catch (error) {
|
|
876
|
-
console.warn('Error evaluating conditions for candidate:', error);
|
|
877
|
-
return [];
|
|
878
|
-
}
|
|
879
|
-
},
|
|
880
|
-
[]
|
|
881
|
-
);
|
|
882
|
-
|
|
883
|
-
// Resolve the currently selected resource
|
|
884
|
-
const resolveSelectedResource = useCallback(
|
|
885
|
-
(resolver: Runtime.ResourceResolver, resourceId: string) => {
|
|
886
|
-
if (
|
|
887
|
-
!activeProcessedResources?.system.resourceManager ||
|
|
888
|
-
!activeProcessedResources?.compiledCollection
|
|
889
|
-
) {
|
|
890
|
-
setResolutionResult({ success: false, error: 'No resource manager available' });
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
const resourceResult = activeProcessedResources.system.resourceManager.getBuiltResource(resourceId);
|
|
895
|
-
if (resourceResult.isFailure()) {
|
|
896
|
-
setResolutionResult({
|
|
897
|
-
success: false,
|
|
898
|
-
error: `Failed to get resource: ${resourceResult.message}`
|
|
899
|
-
});
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const resource = resourceResult.value;
|
|
904
|
-
const compiledCollection = activeProcessedResources.compiledCollection;
|
|
905
|
-
|
|
906
|
-
// Find the compiled resource for condition set keys
|
|
907
|
-
const compiledResource = compiledCollection.resources.find((r) => r.id === resourceId);
|
|
908
|
-
if (!compiledResource) {
|
|
909
|
-
setResolutionResult({
|
|
910
|
-
success: false,
|
|
911
|
-
error: 'Failed to find compiled resource'
|
|
912
|
-
});
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Resolve best candidate
|
|
917
|
-
const bestResult = resolver.resolveResource(resource);
|
|
918
|
-
|
|
919
|
-
// Resolve all candidates
|
|
920
|
-
const allResult = resolver.resolveAllResourceCandidates(resource);
|
|
921
|
-
|
|
922
|
-
// Resolve composed value
|
|
923
|
-
const composedResult = resolver.resolveComposedResourceValue(resource);
|
|
924
|
-
|
|
925
|
-
// Get decision resolution result to understand which candidates are regular vs default matches
|
|
926
|
-
const decisionResult = resolver.resolveDecision(resource.decision.baseDecision);
|
|
927
|
-
const decision = decisionResult.isSuccess() ? decisionResult.value : null;
|
|
928
|
-
|
|
929
|
-
// Build detailed candidate information, preserving priority order for matched candidates
|
|
930
|
-
const candidateDetails: CandidateInfo[] = [];
|
|
931
|
-
const matchedCandidates = allResult.isSuccess() ? allResult.value : [];
|
|
932
|
-
|
|
933
|
-
// Create lookup sets for regular and default matches
|
|
934
|
-
const regularMatchIndices = new Set(decision?.success ? decision.instanceIndices : []);
|
|
935
|
-
const defaultMatchIndices = new Set(decision?.success ? decision.defaultInstanceIndices : []);
|
|
936
|
-
|
|
937
|
-
// First, add matched candidates in priority order (regular matches first, then defaults)
|
|
938
|
-
matchedCandidates.forEach((matchedCandidate) => {
|
|
939
|
-
// Find the index by comparing candidate objects
|
|
940
|
-
const index = resource.candidates.findIndex((candidate) => candidate === matchedCandidate);
|
|
941
|
-
if (index !== -1) {
|
|
942
|
-
const conditionSetKey = getCandidateConditionSetKey(index, compiledResource, compiledCollection);
|
|
943
|
-
const conditionEvaluations = evaluateConditionsForCandidate(
|
|
944
|
-
resolver,
|
|
945
|
-
index,
|
|
946
|
-
compiledResource,
|
|
947
|
-
compiledCollection
|
|
948
|
-
);
|
|
949
|
-
|
|
950
|
-
const isDefaultMatch = defaultMatchIndices.has(index);
|
|
951
|
-
const isRegularMatch = regularMatchIndices.has(index);
|
|
952
|
-
|
|
953
|
-
// Determine overall match type for this candidate
|
|
954
|
-
const candidateMatchType: ConditionMatchType = isRegularMatch
|
|
955
|
-
? 'match'
|
|
956
|
-
: isDefaultMatch
|
|
957
|
-
? 'matchAsDefault'
|
|
958
|
-
: 'noMatch';
|
|
959
|
-
|
|
960
|
-
candidateDetails.push({
|
|
961
|
-
candidate: resource.candidates[index],
|
|
962
|
-
conditionSetKey,
|
|
963
|
-
candidateIndex: index,
|
|
964
|
-
matched: true,
|
|
965
|
-
matchType: candidateMatchType,
|
|
966
|
-
isDefaultMatch,
|
|
967
|
-
conditionEvaluations
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
// Then, add non-matching candidates in their original order
|
|
973
|
-
resource.candidates.forEach((candidate, index) => {
|
|
974
|
-
const isMatched = matchedCandidates.some((mc) => mc === candidate);
|
|
975
|
-
if (!isMatched) {
|
|
976
|
-
const conditionSetKey = getCandidateConditionSetKey(index, compiledResource, compiledCollection);
|
|
977
|
-
const conditionEvaluations = evaluateConditionsForCandidate(
|
|
978
|
-
resolver,
|
|
979
|
-
index,
|
|
980
|
-
compiledResource,
|
|
981
|
-
compiledCollection
|
|
982
|
-
);
|
|
983
|
-
|
|
984
|
-
candidateDetails.push({
|
|
985
|
-
candidate,
|
|
986
|
-
conditionSetKey,
|
|
987
|
-
candidateIndex: index,
|
|
988
|
-
matched: false,
|
|
989
|
-
matchType: 'noMatch',
|
|
990
|
-
isDefaultMatch: false,
|
|
991
|
-
conditionEvaluations
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
if (bestResult.isFailure() && allResult.isFailure()) {
|
|
997
|
-
setResolutionResult({
|
|
998
|
-
success: false,
|
|
999
|
-
resource,
|
|
1000
|
-
candidateDetails,
|
|
1001
|
-
error: `Resolution failed: ${bestResult.message}`
|
|
1002
|
-
});
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
setResolutionResult({
|
|
1007
|
-
success: true,
|
|
1008
|
-
resource,
|
|
1009
|
-
bestCandidate: bestResult.isSuccess() ? bestResult.value : undefined,
|
|
1010
|
-
allCandidates: allResult.isSuccess() ? allResult.value : undefined,
|
|
1011
|
-
candidateDetails,
|
|
1012
|
-
composedValue: composedResult.isSuccess() ? composedResult.value : undefined,
|
|
1013
|
-
error: bestResult.isFailure() ? bestResult.message : undefined
|
|
1014
|
-
});
|
|
1015
|
-
},
|
|
1016
|
-
[
|
|
1017
|
-
activeProcessedResources?.system.resourceManager,
|
|
1018
|
-
activeProcessedResources?.compiledCollection,
|
|
1019
|
-
getCandidateConditionSetKey,
|
|
1020
|
-
evaluateConditionsForCandidate
|
|
1021
|
-
]
|
|
1022
|
-
);
|
|
1023
|
-
|
|
1024
|
-
// Handle resource selection
|
|
1025
|
-
const handleResourceSelect = useCallback(
|
|
1026
|
-
(resourceId: string) => {
|
|
1027
|
-
setSelectedResourceId(resourceId);
|
|
1028
|
-
setResolutionResult(null);
|
|
1029
|
-
|
|
1030
|
-
if (currentResolver) {
|
|
1031
|
-
resolveSelectedResource(currentResolver, resourceId);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
onMessage?.('info', `Selected resource: ${resourceId}`);
|
|
1035
|
-
},
|
|
1036
|
-
[currentResolver, resolveSelectedResource, onMessage]
|
|
1037
|
-
);
|
|
1038
|
-
|
|
1039
|
-
// Handle context value change
|
|
1040
|
-
const handleContextChange = useCallback((qualifierName: string, value: string) => {
|
|
1041
|
-
setPendingContextValues((prev) => ({
|
|
1042
|
-
...prev,
|
|
1043
|
-
[qualifierName]: value
|
|
1044
|
-
}));
|
|
1045
|
-
}, []);
|
|
1046
|
-
|
|
1047
|
-
// Handle cache reset
|
|
1048
|
-
const handleCacheReset = useCallback(() => {
|
|
1049
|
-
if (currentResolver && cacheMetrics) {
|
|
1050
|
-
currentResolver.clearConditionCache();
|
|
1051
|
-
cacheMetrics.reset();
|
|
1052
|
-
onMessage?.('info', 'Cache cleared and metrics reset');
|
|
1053
|
-
}
|
|
1054
|
-
}, [currentResolver, cacheMetrics, onMessage]);
|
|
1055
|
-
|
|
1056
|
-
// Reset auto-apply flag when defaults change
|
|
1057
|
-
React.useEffect(() => {
|
|
1058
|
-
setHasAutoApplied(false);
|
|
1059
|
-
}, [defaultContextValues]);
|
|
1060
|
-
|
|
1061
|
-
// Auto-apply default context when resources are loaded or defaults change
|
|
1062
|
-
React.useEffect(() => {
|
|
1063
|
-
if (activeProcessedResources?.system && Object.keys(defaultContextValues).length > 0 && !hasAutoApplied) {
|
|
1064
|
-
// Apply the default context automatically
|
|
1065
|
-
applyContext();
|
|
1066
|
-
setHasAutoApplied(true);
|
|
1067
|
-
}
|
|
1068
|
-
}, [activeProcessedResources?.system, defaultContextValues, hasAutoApplied, applyContext]);
|
|
1069
|
-
|
|
1070
|
-
if (!resourceState.processedResources) {
|
|
1071
|
-
return (
|
|
1072
|
-
<div className="p-6">
|
|
1073
|
-
<div className="flex items-center space-x-3 mb-6">
|
|
1074
|
-
<MagnifyingGlassIcon className="h-8 w-8 text-blue-600" />
|
|
1075
|
-
<h2 className="text-2xl font-bold text-gray-900">Resolution Viewer</h2>
|
|
1076
|
-
</div>
|
|
1077
|
-
|
|
1078
|
-
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
|
|
1079
|
-
<div className="max-w-2xl mx-auto">
|
|
1080
|
-
<h3 className="text-xl font-semibold text-gray-900 mb-4">No Resources Loaded</h3>
|
|
1081
|
-
<p className="text-gray-600 mb-6">
|
|
1082
|
-
Import resources to test resource resolution with different contexts.
|
|
1083
|
-
</p>
|
|
1084
|
-
<div className="bg-blue-50 rounded-lg p-4">
|
|
1085
|
-
<p className="text-sm text-blue-800">
|
|
1086
|
-
<strong>Tip:</strong> Use the Import Tool to load resources, then return here to test how they
|
|
1087
|
-
resolve with different qualifier contexts.
|
|
1088
|
-
</p>
|
|
1089
|
-
</div>
|
|
1090
|
-
</div>
|
|
1091
|
-
</div>
|
|
1092
|
-
</div>
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
return (
|
|
1097
|
-
<div className="p-6">
|
|
1098
|
-
<div className="flex items-center space-x-3 mb-6">
|
|
1099
|
-
<MagnifyingGlassIcon className="h-8 w-8 text-blue-600" />
|
|
1100
|
-
<h2 className="text-2xl font-bold text-gray-900">Resolution Viewer</h2>
|
|
1101
|
-
{isFilteringActive && (
|
|
1102
|
-
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
|
1103
|
-
Filtered
|
|
1104
|
-
</span>
|
|
1105
|
-
)}
|
|
1106
|
-
</div>
|
|
1107
|
-
|
|
1108
|
-
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
1109
|
-
{/* Context Input Panel */}
|
|
1110
|
-
<div className="mb-6">
|
|
1111
|
-
<h3 className="text-lg font-semibold text-gray-900 mb-4">Context Configuration</h3>
|
|
1112
|
-
<div className="bg-gray-50 rounded-lg p-4">
|
|
1113
|
-
<div className="mb-4">
|
|
1114
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
1115
|
-
{availableQualifiers.map((qualifierName) => {
|
|
1116
|
-
const typeInfo = qualifierTypeInfo[qualifierName];
|
|
1117
|
-
const hasEnumeratedValues =
|
|
1118
|
-
typeInfo?.enumeratedValues && typeInfo.enumeratedValues.length > 0;
|
|
1119
|
-
|
|
1120
|
-
return (
|
|
1121
|
-
<div key={qualifierName} className="bg-white rounded border border-gray-200 p-2">
|
|
1122
|
-
<div className="flex items-center gap-2">
|
|
1123
|
-
<label className="text-sm font-medium text-gray-700 min-w-0 flex-shrink-0">
|
|
1124
|
-
{qualifierName}:
|
|
1125
|
-
</label>
|
|
1126
|
-
{hasEnumeratedValues ? (
|
|
1127
|
-
<select
|
|
1128
|
-
value={pendingContextValues[qualifierName] || ''}
|
|
1129
|
-
onChange={(e) => handleContextChange(qualifierName, e.target.value)}
|
|
1130
|
-
className="flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm min-w-0 bg-white"
|
|
1131
|
-
>
|
|
1132
|
-
<option value="" disabled>
|
|
1133
|
-
Select {qualifierName}...
|
|
1134
|
-
</option>
|
|
1135
|
-
{typeInfo!.enumeratedValues!.map((value) => (
|
|
1136
|
-
<option key={value} value={value}>
|
|
1137
|
-
{value}
|
|
1138
|
-
</option>
|
|
1139
|
-
))}
|
|
1140
|
-
</select>
|
|
1141
|
-
) : (
|
|
1142
|
-
<input
|
|
1143
|
-
type="text"
|
|
1144
|
-
value={pendingContextValues[qualifierName] || ''}
|
|
1145
|
-
onChange={(e) => handleContextChange(qualifierName, e.target.value)}
|
|
1146
|
-
className="flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm min-w-0"
|
|
1147
|
-
placeholder={`Enter ${qualifierName} value`}
|
|
1148
|
-
/>
|
|
1149
|
-
)}
|
|
1150
|
-
</div>
|
|
1151
|
-
</div>
|
|
1152
|
-
);
|
|
1153
|
-
})}
|
|
1154
|
-
</div>
|
|
1155
|
-
</div>
|
|
1156
|
-
<div className="flex items-center justify-between">
|
|
1157
|
-
<div className="text-sm text-gray-600">
|
|
1158
|
-
Current:{' '}
|
|
1159
|
-
{Object.entries(contextValues)
|
|
1160
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
1161
|
-
.join(', ')}
|
|
1162
|
-
</div>
|
|
1163
|
-
<button
|
|
1164
|
-
onClick={applyContext}
|
|
1165
|
-
disabled={!hasPendingChanges}
|
|
1166
|
-
className={`px-4 py-2 rounded-md text-sm font-medium ${
|
|
1167
|
-
hasPendingChanges
|
|
1168
|
-
? 'bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500'
|
|
1169
|
-
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
|
1170
|
-
}`}
|
|
1171
|
-
>
|
|
1172
|
-
{hasPendingChanges ? 'Apply Changes' : currentResolver ? 'Context Applied' : 'Apply Context'}
|
|
1173
|
-
</button>
|
|
1174
|
-
</div>
|
|
1175
|
-
</div>
|
|
1176
|
-
</div>
|
|
1177
|
-
|
|
1178
|
-
{/* Cache Dashboard */}
|
|
1179
|
-
<CacheDashboard cacheMetrics={cacheMetrics} resolver={currentResolver} onReset={handleCacheReset} />
|
|
1180
|
-
|
|
1181
|
-
{/* Main Browser/Details Layout */}
|
|
1182
|
-
<div className="flex flex-col lg:flex-row gap-6 h-[600px]">
|
|
1183
|
-
{/* Left side: Resource Selection and Cache Contents */}
|
|
1184
|
-
<div className="lg:w-1/2 flex flex-col">
|
|
1185
|
-
<div className="flex items-center justify-between mb-4">
|
|
1186
|
-
<h3 className="text-lg font-semibold text-gray-900">Resources</h3>
|
|
1187
|
-
<div className="text-sm text-gray-500">{availableResources.length} available</div>
|
|
1188
|
-
</div>
|
|
1189
|
-
|
|
1190
|
-
<div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg bg-gray-50 mb-4">
|
|
1191
|
-
{availableResources.map((resourceId) => (
|
|
1192
|
-
<div
|
|
1193
|
-
key={resourceId}
|
|
1194
|
-
className={`flex items-center px-3 py-2 cursor-pointer hover:bg-gray-100 ${
|
|
1195
|
-
selectedResourceId === resourceId ? 'bg-blue-50 border-r-2 border-blue-500' : ''
|
|
1196
|
-
}`}
|
|
1197
|
-
onClick={() => handleResourceSelect(resourceId)}
|
|
1198
|
-
>
|
|
1199
|
-
<DocumentTextIcon className="w-4 h-4 mr-2 text-green-500" />
|
|
1200
|
-
<span
|
|
1201
|
-
className={`text-sm ${
|
|
1202
|
-
selectedResourceId === resourceId ? 'font-medium text-blue-900' : 'text-gray-700'
|
|
1203
|
-
}`}
|
|
1204
|
-
>
|
|
1205
|
-
{resourceId}
|
|
1206
|
-
</span>
|
|
1207
|
-
</div>
|
|
1208
|
-
))}
|
|
1209
|
-
</div>
|
|
1210
|
-
|
|
1211
|
-
{/* Cache Contents Section */}
|
|
1212
|
-
<CacheContentsDisplay resolver={currentResolver} />
|
|
1213
|
-
</div>
|
|
1214
|
-
|
|
1215
|
-
{/* Right side: Resolution Results */}
|
|
1216
|
-
<div className="lg:w-1/2 flex flex-col">
|
|
1217
|
-
<div className="flex items-center justify-between mb-4">
|
|
1218
|
-
<h3 className="text-lg font-semibold text-gray-900">Results</h3>
|
|
1219
|
-
{selectedResourceId && (
|
|
1220
|
-
<div className="flex space-x-2">
|
|
1221
|
-
<button
|
|
1222
|
-
onClick={() => setViewMode('best')}
|
|
1223
|
-
className={`px-3 py-1 text-xs rounded ${
|
|
1224
|
-
viewMode === 'best' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
|
|
1225
|
-
}`}
|
|
1226
|
-
>
|
|
1227
|
-
Best
|
|
1228
|
-
</button>
|
|
1229
|
-
<button
|
|
1230
|
-
onClick={() => setViewMode('all')}
|
|
1231
|
-
className={`px-3 py-1 text-xs rounded ${
|
|
1232
|
-
viewMode === 'all' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
|
|
1233
|
-
}`}
|
|
1234
|
-
>
|
|
1235
|
-
All
|
|
1236
|
-
</button>
|
|
1237
|
-
<button
|
|
1238
|
-
onClick={() => setViewMode('composed')}
|
|
1239
|
-
className={`px-3 py-1 text-xs rounded ${
|
|
1240
|
-
viewMode === 'composed' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
|
|
1241
|
-
}`}
|
|
1242
|
-
>
|
|
1243
|
-
Composed
|
|
1244
|
-
</button>
|
|
1245
|
-
<button
|
|
1246
|
-
onClick={() => setViewMode('raw')}
|
|
1247
|
-
className={`px-3 py-1 text-xs rounded ${
|
|
1248
|
-
viewMode === 'raw' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
|
|
1249
|
-
}`}
|
|
1250
|
-
>
|
|
1251
|
-
Raw
|
|
1252
|
-
</button>
|
|
1253
|
-
</div>
|
|
1254
|
-
)}
|
|
1255
|
-
</div>
|
|
1256
|
-
|
|
1257
|
-
<div className="flex-1 overflow-y-auto border border-gray-200 rounded-lg p-4 bg-gray-50">
|
|
1258
|
-
{!selectedResourceId ? (
|
|
1259
|
-
<div className="flex items-center justify-center h-full">
|
|
1260
|
-
<div className="text-center">
|
|
1261
|
-
<CubeIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
1262
|
-
<p className="text-gray-500">Select a resource to view resolution results</p>
|
|
1263
|
-
</div>
|
|
1264
|
-
</div>
|
|
1265
|
-
) : !currentResolver ? (
|
|
1266
|
-
<div className="text-center text-gray-500">
|
|
1267
|
-
<p>Apply a context to resolve resources</p>
|
|
1268
|
-
</div>
|
|
1269
|
-
) : !resolutionResult ? (
|
|
1270
|
-
<div className="text-center text-gray-500">
|
|
1271
|
-
<p>Resolving...</p>
|
|
1272
|
-
</div>
|
|
1273
|
-
) : (
|
|
1274
|
-
<ResolutionResults
|
|
1275
|
-
result={resolutionResult}
|
|
1276
|
-
viewMode={viewMode}
|
|
1277
|
-
contextValues={contextValues}
|
|
1278
|
-
/>
|
|
1279
|
-
)}
|
|
1280
|
-
</div>
|
|
1281
|
-
</div>
|
|
1282
|
-
</div>
|
|
1283
|
-
</div>
|
|
1284
|
-
</div>
|
|
1285
|
-
);
|
|
1286
|
-
};
|
|
1287
|
-
|
|
1288
|
-
// Resolution Results Component
|
|
1289
|
-
interface ResolutionResultsProps {
|
|
1290
|
-
result: ResolutionResult;
|
|
1291
|
-
viewMode: ViewMode;
|
|
1292
|
-
contextValues: ContextState;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
const ResolutionResults: React.FC<ResolutionResultsProps> = ({ result, viewMode, contextValues }) => {
|
|
1296
|
-
if (!result.success) {
|
|
1297
|
-
return (
|
|
1298
|
-
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
1299
|
-
<h4 className="font-medium text-red-800 mb-2">Resolution Failed</h4>
|
|
1300
|
-
<p className="text-sm text-red-600">{result.error}</p>
|
|
1301
|
-
</div>
|
|
1302
|
-
);
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
if (viewMode === 'raw') {
|
|
1306
|
-
return (
|
|
1307
|
-
<div className="space-y-4">
|
|
1308
|
-
<div>
|
|
1309
|
-
<h4 className="font-medium text-gray-800 mb-2">Raw Resolution Data</h4>
|
|
1310
|
-
<pre className="text-xs bg-white p-3 rounded border overflow-x-auto">
|
|
1311
|
-
{JSON.stringify(
|
|
1312
|
-
{
|
|
1313
|
-
context: contextValues,
|
|
1314
|
-
resource: result.resource
|
|
1315
|
-
? {
|
|
1316
|
-
id: result.resource.id,
|
|
1317
|
-
candidateCount: result.resource.candidates.length
|
|
1318
|
-
}
|
|
1319
|
-
: null,
|
|
1320
|
-
bestCandidate: result.bestCandidate?.json,
|
|
1321
|
-
allCandidates: result.allCandidates?.map((c) => c.json),
|
|
1322
|
-
composedValue: result.composedValue,
|
|
1323
|
-
error: result.error
|
|
1324
|
-
},
|
|
1325
|
-
null,
|
|
1326
|
-
2
|
|
1327
|
-
)}
|
|
1328
|
-
</pre>
|
|
1329
|
-
</div>
|
|
1330
|
-
</div>
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
if (viewMode === 'composed') {
|
|
1335
|
-
return (
|
|
1336
|
-
<div className="space-y-4">
|
|
1337
|
-
<div>
|
|
1338
|
-
<h4 className="font-medium text-gray-800 mb-2">Composed Resource Value</h4>
|
|
1339
|
-
{result.composedValue ? (
|
|
1340
|
-
<div className="bg-white p-3 rounded border">
|
|
1341
|
-
<div className="text-sm font-medium text-gray-700 mb-2">
|
|
1342
|
-
Composed value from all matching candidates
|
|
1343
|
-
</div>
|
|
1344
|
-
<pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto">
|
|
1345
|
-
{JSON.stringify(result.composedValue, null, 2)}
|
|
1346
|
-
</pre>
|
|
1347
|
-
</div>
|
|
1348
|
-
) : (
|
|
1349
|
-
<div className="bg-yellow-50 border border-yellow-200 rounded p-3">
|
|
1350
|
-
<p className="text-sm text-yellow-800">No composed value available for the current context.</p>
|
|
1351
|
-
{result.error && <p className="text-xs text-yellow-600 mt-1">{result.error}</p>}
|
|
1352
|
-
</div>
|
|
1353
|
-
)}
|
|
1354
|
-
</div>
|
|
1355
|
-
|
|
1356
|
-
{result.resource && (
|
|
1357
|
-
<div>
|
|
1358
|
-
<h4 className="font-medium text-gray-800 mb-2">Resource Info</h4>
|
|
1359
|
-
<div className="bg-white p-3 rounded border text-sm">
|
|
1360
|
-
<div>
|
|
1361
|
-
<strong>ID:</strong> {result.resource.id}
|
|
1362
|
-
</div>
|
|
1363
|
-
<div>
|
|
1364
|
-
<strong>Type:</strong> {result.resource.resourceType.key}
|
|
1365
|
-
</div>
|
|
1366
|
-
<div>
|
|
1367
|
-
<strong>Total Candidates:</strong> {result.resource.candidates.length}
|
|
1368
|
-
</div>
|
|
1369
|
-
</div>
|
|
1370
|
-
</div>
|
|
1371
|
-
)}
|
|
1372
|
-
</div>
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
if (viewMode === 'all') {
|
|
1377
|
-
const regularMatchingCandidates =
|
|
1378
|
-
result.candidateDetails?.filter((c) => c.matched && !c.isDefaultMatch) || [];
|
|
1379
|
-
const defaultMatchingCandidates =
|
|
1380
|
-
result.candidateDetails?.filter((c) => c.matched && c.isDefaultMatch) || [];
|
|
1381
|
-
const nonMatchingCandidates = result.candidateDetails?.filter((c) => !c.matched) || [];
|
|
1382
|
-
|
|
1383
|
-
const getMatchTypeColor = (type: ConditionMatchType) => {
|
|
1384
|
-
switch (type) {
|
|
1385
|
-
case 'match':
|
|
1386
|
-
return 'bg-green-100 text-green-800';
|
|
1387
|
-
case 'matchAsDefault':
|
|
1388
|
-
return 'bg-amber-100 text-amber-800';
|
|
1389
|
-
case 'noMatch':
|
|
1390
|
-
return 'bg-red-100 text-red-800';
|
|
1391
|
-
default:
|
|
1392
|
-
return 'bg-gray-100 text-gray-800';
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
|
|
1396
|
-
const getMatchTypeIcon = (type: ConditionMatchType) => {
|
|
1397
|
-
switch (type) {
|
|
1398
|
-
case 'match':
|
|
1399
|
-
return '✓';
|
|
1400
|
-
case 'matchAsDefault':
|
|
1401
|
-
return '≈';
|
|
1402
|
-
case 'noMatch':
|
|
1403
|
-
return '✗';
|
|
1404
|
-
default:
|
|
1405
|
-
return '?';
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
1408
|
-
|
|
1409
|
-
return (
|
|
1410
|
-
<div className="space-y-4">
|
|
1411
|
-
{/* Regular Matching Candidates */}
|
|
1412
|
-
{regularMatchingCandidates.length > 0 && (
|
|
1413
|
-
<div>
|
|
1414
|
-
<h4 className="font-medium text-gray-800 mb-2">Regular Matches</h4>
|
|
1415
|
-
<div className="space-y-2">
|
|
1416
|
-
{regularMatchingCandidates.map((candidateInfo, index) => (
|
|
1417
|
-
<div
|
|
1418
|
-
key={candidateInfo.candidateIndex}
|
|
1419
|
-
className="bg-white p-3 rounded border border-green-200"
|
|
1420
|
-
>
|
|
1421
|
-
<div className="flex items-center justify-between mb-2">
|
|
1422
|
-
<div className="text-sm font-medium text-gray-700 flex items-center space-x-2">
|
|
1423
|
-
<span>
|
|
1424
|
-
Candidate {candidateInfo.candidateIndex + 1} {index === 0 ? '(Best Match)' : ''}
|
|
1425
|
-
</span>
|
|
1426
|
-
{candidateInfo.conditionSetKey && (
|
|
1427
|
-
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
1428
|
-
{candidateInfo.conditionSetKey}
|
|
1429
|
-
</span>
|
|
1430
|
-
)}
|
|
1431
|
-
<span
|
|
1432
|
-
className={`px-2 py-1 rounded text-xs ${getMatchTypeColor(candidateInfo.matchType)}`}
|
|
1433
|
-
>
|
|
1434
|
-
{getMatchTypeIcon(candidateInfo.matchType)} {candidateInfo.matchType}
|
|
1435
|
-
</span>
|
|
1436
|
-
</div>
|
|
1437
|
-
<div className="flex items-center space-x-2 text-xs">
|
|
1438
|
-
{candidateInfo.candidate.isPartial && (
|
|
1439
|
-
<span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Partial</span>
|
|
1440
|
-
)}
|
|
1441
|
-
{candidateInfo.candidate.mergeMethod && (
|
|
1442
|
-
<span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
|
|
1443
|
-
{candidateInfo.candidate.mergeMethod}
|
|
1444
|
-
</span>
|
|
1445
|
-
)}
|
|
1446
|
-
</div>
|
|
1447
|
-
</div>
|
|
1448
|
-
<pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto mt-2">
|
|
1449
|
-
{JSON.stringify(candidateInfo.candidate.json, null, 2)}
|
|
1450
|
-
</pre>
|
|
1451
|
-
{candidateInfo.conditionEvaluations && (
|
|
1452
|
-
<ConditionEvaluationDisplay evaluations={candidateInfo.conditionEvaluations} />
|
|
1453
|
-
)}
|
|
1454
|
-
</div>
|
|
1455
|
-
))}
|
|
1456
|
-
</div>
|
|
1457
|
-
</div>
|
|
1458
|
-
)}
|
|
1459
|
-
|
|
1460
|
-
{/* Default Matching Candidates */}
|
|
1461
|
-
{defaultMatchingCandidates.length > 0 && (
|
|
1462
|
-
<div>
|
|
1463
|
-
<h4 className="font-medium text-gray-800 mb-2">Default Matches</h4>
|
|
1464
|
-
<div className="space-y-2">
|
|
1465
|
-
{defaultMatchingCandidates.map((candidateInfo, index) => (
|
|
1466
|
-
<div
|
|
1467
|
-
key={candidateInfo.candidateIndex}
|
|
1468
|
-
className="bg-white p-3 rounded border border-amber-200"
|
|
1469
|
-
>
|
|
1470
|
-
<div className="flex items-center justify-between mb-2">
|
|
1471
|
-
<div className="text-sm font-medium text-gray-700 flex items-center space-x-2">
|
|
1472
|
-
<span>Candidate {candidateInfo.candidateIndex + 1}</span>
|
|
1473
|
-
{candidateInfo.conditionSetKey && (
|
|
1474
|
-
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
1475
|
-
{candidateInfo.conditionSetKey}
|
|
1476
|
-
</span>
|
|
1477
|
-
)}
|
|
1478
|
-
<span
|
|
1479
|
-
className={`px-2 py-1 rounded text-xs ${getMatchTypeColor(candidateInfo.matchType)}`}
|
|
1480
|
-
>
|
|
1481
|
-
{getMatchTypeIcon(candidateInfo.matchType)} {candidateInfo.matchType}
|
|
1482
|
-
</span>
|
|
1483
|
-
</div>
|
|
1484
|
-
<div className="flex items-center space-x-2 text-xs">
|
|
1485
|
-
{candidateInfo.candidate.isPartial && (
|
|
1486
|
-
<span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Partial</span>
|
|
1487
|
-
)}
|
|
1488
|
-
{candidateInfo.candidate.mergeMethod && (
|
|
1489
|
-
<span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
|
|
1490
|
-
{candidateInfo.candidate.mergeMethod}
|
|
1491
|
-
</span>
|
|
1492
|
-
)}
|
|
1493
|
-
</div>
|
|
1494
|
-
</div>
|
|
1495
|
-
<pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto mt-2">
|
|
1496
|
-
{JSON.stringify(candidateInfo.candidate.json, null, 2)}
|
|
1497
|
-
</pre>
|
|
1498
|
-
{candidateInfo.conditionEvaluations && (
|
|
1499
|
-
<ConditionEvaluationDisplay evaluations={candidateInfo.conditionEvaluations} />
|
|
1500
|
-
)}
|
|
1501
|
-
</div>
|
|
1502
|
-
))}
|
|
1503
|
-
</div>
|
|
1504
|
-
</div>
|
|
1505
|
-
)}
|
|
1506
|
-
|
|
1507
|
-
{/* Show message when no matches */}
|
|
1508
|
-
{regularMatchingCandidates.length === 0 && defaultMatchingCandidates.length === 0 && (
|
|
1509
|
-
<div>
|
|
1510
|
-
<h4 className="font-medium text-gray-800 mb-2">Matching Candidates</h4>
|
|
1511
|
-
<p className="text-sm text-gray-600">No candidates matched the current context.</p>
|
|
1512
|
-
</div>
|
|
1513
|
-
)}
|
|
1514
|
-
|
|
1515
|
-
{/* Non-matching Candidates */}
|
|
1516
|
-
|
|
1517
|
-
{nonMatchingCandidates.length > 0 && (
|
|
1518
|
-
<div>
|
|
1519
|
-
<h4 className="font-medium text-gray-500 mb-2">Non-matching Candidates</h4>
|
|
1520
|
-
<div className="space-y-2">
|
|
1521
|
-
{nonMatchingCandidates.map((candidateInfo) => (
|
|
1522
|
-
<div
|
|
1523
|
-
key={candidateInfo.candidateIndex}
|
|
1524
|
-
className="bg-gray-50 p-3 rounded border border-gray-200 opacity-75"
|
|
1525
|
-
>
|
|
1526
|
-
<div className="flex items-center justify-between mb-2">
|
|
1527
|
-
<div className="text-sm font-medium text-gray-500">
|
|
1528
|
-
Candidate {candidateInfo.candidateIndex + 1}
|
|
1529
|
-
{candidateInfo.conditionSetKey && (
|
|
1530
|
-
<span className="ml-2 text-xs bg-gray-200 text-gray-600 px-2 py-1 rounded">
|
|
1531
|
-
{candidateInfo.conditionSetKey}
|
|
1532
|
-
</span>
|
|
1533
|
-
)}
|
|
1534
|
-
</div>
|
|
1535
|
-
<div className="flex items-center space-x-2 text-xs">
|
|
1536
|
-
{candidateInfo.candidate.isPartial && (
|
|
1537
|
-
<span className="bg-yellow-200 text-yellow-700 px-2 py-1 rounded">Partial</span>
|
|
1538
|
-
)}
|
|
1539
|
-
{candidateInfo.candidate.mergeMethod && (
|
|
1540
|
-
<span className="bg-gray-200 text-gray-600 px-2 py-1 rounded">
|
|
1541
|
-
{candidateInfo.candidate.mergeMethod}
|
|
1542
|
-
</span>
|
|
1543
|
-
)}
|
|
1544
|
-
</div>
|
|
1545
|
-
</div>
|
|
1546
|
-
<pre className="text-xs bg-gray-100 p-2 rounded overflow-x-auto text-gray-600">
|
|
1547
|
-
{JSON.stringify(candidateInfo.candidate.json, null, 2)}
|
|
1548
|
-
</pre>
|
|
1549
|
-
{candidateInfo.conditionEvaluations && (
|
|
1550
|
-
<ConditionEvaluationDisplay evaluations={candidateInfo.conditionEvaluations} />
|
|
1551
|
-
)}
|
|
1552
|
-
</div>
|
|
1553
|
-
))}
|
|
1554
|
-
</div>
|
|
1555
|
-
</div>
|
|
1556
|
-
)}
|
|
1557
|
-
</div>
|
|
1558
|
-
);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// Best candidate view
|
|
1562
|
-
const bestCandidateInfo = result.candidateDetails?.find((c) => c.matched);
|
|
1563
|
-
|
|
1564
|
-
const getMatchTypeColor = (type: ConditionMatchType) => {
|
|
1565
|
-
switch (type) {
|
|
1566
|
-
case 'match':
|
|
1567
|
-
return 'bg-green-100 text-green-800';
|
|
1568
|
-
case 'matchAsDefault':
|
|
1569
|
-
return 'bg-amber-100 text-amber-800';
|
|
1570
|
-
case 'noMatch':
|
|
1571
|
-
return 'bg-red-100 text-red-800';
|
|
1572
|
-
default:
|
|
1573
|
-
return 'bg-gray-100 text-gray-800';
|
|
1574
|
-
}
|
|
1575
|
-
};
|
|
1576
|
-
|
|
1577
|
-
const getMatchTypeIcon = (type: ConditionMatchType) => {
|
|
1578
|
-
switch (type) {
|
|
1579
|
-
case 'match':
|
|
1580
|
-
return '✓';
|
|
1581
|
-
case 'matchAsDefault':
|
|
1582
|
-
return '≈';
|
|
1583
|
-
case 'noMatch':
|
|
1584
|
-
return '✗';
|
|
1585
|
-
default:
|
|
1586
|
-
return '?';
|
|
1587
|
-
}
|
|
1588
|
-
};
|
|
1589
|
-
|
|
1590
|
-
const getMatchDescription = (candidateInfo: CandidateInfo | undefined) => {
|
|
1591
|
-
if (!candidateInfo) return 'Selected candidate for current context';
|
|
1592
|
-
|
|
1593
|
-
if (candidateInfo.isDefaultMatch) {
|
|
1594
|
-
return 'Selected candidate (matched via default values)';
|
|
1595
|
-
} else {
|
|
1596
|
-
return 'Selected candidate (direct match)';
|
|
1597
|
-
}
|
|
1598
|
-
};
|
|
1599
|
-
|
|
1600
|
-
return (
|
|
1601
|
-
<div className="space-y-4">
|
|
1602
|
-
<div>
|
|
1603
|
-
<h4 className="font-medium text-gray-800 mb-2">Best Match</h4>
|
|
1604
|
-
{result.bestCandidate ? (
|
|
1605
|
-
<div
|
|
1606
|
-
className={`bg-white p-3 rounded border ${
|
|
1607
|
-
bestCandidateInfo?.isDefaultMatch ? 'border-amber-200' : 'border-green-200'
|
|
1608
|
-
}`}
|
|
1609
|
-
>
|
|
1610
|
-
<div className="flex items-center justify-between mb-2">
|
|
1611
|
-
<div className="text-sm font-medium text-gray-700 flex items-center space-x-2">
|
|
1612
|
-
<span>{getMatchDescription(bestCandidateInfo)}</span>
|
|
1613
|
-
{bestCandidateInfo?.conditionSetKey && (
|
|
1614
|
-
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
1615
|
-
{bestCandidateInfo.conditionSetKey}
|
|
1616
|
-
</span>
|
|
1617
|
-
)}
|
|
1618
|
-
{bestCandidateInfo && (
|
|
1619
|
-
<span
|
|
1620
|
-
className={`px-2 py-1 rounded text-xs ${getMatchTypeColor(bestCandidateInfo.matchType)}`}
|
|
1621
|
-
>
|
|
1622
|
-
{getMatchTypeIcon(bestCandidateInfo.matchType)} {bestCandidateInfo.matchType}
|
|
1623
|
-
</span>
|
|
1624
|
-
)}
|
|
1625
|
-
</div>
|
|
1626
|
-
<div className="flex items-center space-x-2 text-xs">
|
|
1627
|
-
{result.bestCandidate.isPartial && (
|
|
1628
|
-
<span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Partial</span>
|
|
1629
|
-
)}
|
|
1630
|
-
{result.bestCandidate.mergeMethod && (
|
|
1631
|
-
<span className="bg-gray-100 text-gray-700 px-2 py-1 rounded">
|
|
1632
|
-
{result.bestCandidate.mergeMethod}
|
|
1633
|
-
</span>
|
|
1634
|
-
)}
|
|
1635
|
-
</div>
|
|
1636
|
-
</div>
|
|
1637
|
-
<pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto">
|
|
1638
|
-
{JSON.stringify(result.bestCandidate.json, null, 2)}
|
|
1639
|
-
</pre>
|
|
1640
|
-
{bestCandidateInfo?.conditionEvaluations && (
|
|
1641
|
-
<ConditionEvaluationDisplay evaluations={bestCandidateInfo.conditionEvaluations} />
|
|
1642
|
-
)}
|
|
1643
|
-
</div>
|
|
1644
|
-
) : (
|
|
1645
|
-
<div className="bg-yellow-50 border border-yellow-200 rounded p-3">
|
|
1646
|
-
<p className="text-sm text-yellow-800">No best candidate found for the current context.</p>
|
|
1647
|
-
{result.error && <p className="text-xs text-yellow-600 mt-1">{result.error}</p>}
|
|
1648
|
-
</div>
|
|
1649
|
-
)}
|
|
1650
|
-
</div>
|
|
1651
|
-
|
|
1652
|
-
{result.resource && (
|
|
1653
|
-
<div>
|
|
1654
|
-
<h4 className="font-medium text-gray-800 mb-2">Resource Info</h4>
|
|
1655
|
-
<div className="bg-white p-3 rounded border text-sm">
|
|
1656
|
-
<div>
|
|
1657
|
-
<strong>ID:</strong> {result.resource.id}
|
|
1658
|
-
</div>
|
|
1659
|
-
<div>
|
|
1660
|
-
<strong>Type:</strong> {result.resource.resourceType.key}
|
|
1661
|
-
</div>
|
|
1662
|
-
<div>
|
|
1663
|
-
<strong>Total Candidates:</strong> {result.resource.candidates.length}
|
|
1664
|
-
</div>
|
|
1665
|
-
</div>
|
|
1666
|
-
</div>
|
|
1667
|
-
)}
|
|
1668
|
-
</div>
|
|
1669
|
-
);
|
|
1670
|
-
};
|
|
1671
|
-
|
|
1672
|
-
export default ResolutionViewer;
|