@ltm-blueverse/alpha-semantic-hub 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1674 -0
  3. package/explorer/README.md +282 -0
  4. package/explorer/src/App.css +0 -0
  5. package/explorer/src/App.tsx +2035 -0
  6. package/explorer/src/ErrorBoundary.tsx +112 -0
  7. package/explorer/src/ExploreWorkspaceTabs.tsx +29 -0
  8. package/explorer/src/assets/hero.png +0 -0
  9. package/explorer/src/assets/react.svg +1 -0
  10. package/explorer/src/assets/vite.svg +1 -0
  11. package/explorer/src/explorerCapabilities.ts +24 -0
  12. package/explorer/src/index.css +71 -0
  13. package/explorer/src/main.tsx +7 -0
  14. package/explorer/src/store/edgePairKeys.d.ts +2 -0
  15. package/explorer/src/store/edgePairKeys.js +7 -0
  16. package/explorer/src/store/graphStore.ts +183 -0
  17. package/explorer/src/store/registryStore.ts +78 -0
  18. package/explorer/src/types.d.ts +16 -0
  19. package/explorer/src/ui/primitives.css +414 -0
  20. package/explorer/src/ui/primitives.tsx +170 -0
  21. package/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +279 -0
  22. package/explorer/src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx +121 -0
  23. package/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +454 -0
  24. package/explorer/src/workspaces/EnrichWorkspace/RegistryTab.tsx +285 -0
  25. package/explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx +2673 -0
  26. package/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +771 -0
  27. package/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +410 -0
  28. package/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +3584 -0
  29. package/explorer/src/workspaces/GraphWorkspace/MarkdownContentViewer.tsx +705 -0
  30. package/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx +55 -0
  31. package/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx +193 -0
  32. package/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts +29 -0
  33. package/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts +15 -0
  34. package/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts +25 -0
  35. package/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts +15 -0
  36. package/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts +52 -0
  37. package/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts +36 -0
  38. package/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts +41 -0
  39. package/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts +29 -0
  40. package/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts +779 -0
  41. package/explorer/src/workspaces/GraphWorkspace/graphColorLegend.ts +36 -0
  42. package/explorer/src/workspaces/GraphWorkspace/graphConfig.ts +102 -0
  43. package/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts +34 -0
  44. package/explorer/src/workspaces/GraphWorkspace/graphLoading.ts +84 -0
  45. package/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts +775 -0
  46. package/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +2753 -0
  47. package/explorer/src/workspaces/GraphWorkspace/graphStructureLayer.ts +335 -0
  48. package/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +1078 -0
  49. package/explorer/src/workspaces/GraphWorkspace/graphViewCopy.ts +41 -0
  50. package/explorer/src/workspaces/GraphWorkspace/localGraphTransition.ts +9 -0
  51. package/explorer/src/workspaces/GraphWorkspace/markdownEditorState.ts +114 -0
  52. package/explorer/src/workspaces/GraphWorkspace/markdownResourceClient.ts +103 -0
  53. package/explorer/src/workspaces/GraphWorkspace/markdownUrlSafety.ts +29 -0
  54. package/explorer/src/workspaces/GraphWorkspace/nodeMarkdownSync.ts +81 -0
  55. package/explorer/src/workspaces/GraphWorkspace/pluginRegistryPredicates.ts +24 -0
  56. package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx +353 -0
  57. package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx +522 -0
  58. package/explorer/src/workspaces/GraphWorkspace/plugins/index.ts +16 -0
  59. package/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx +120 -0
  60. package/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx +231 -0
  61. package/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx +142 -0
  62. package/explorer/src/workspaces/GraphWorkspace/plugins/types.ts +106 -0
  63. package/explorer/src/workspaces/GraphWorkspace/realtimeGraphAttributes.ts +50 -0
  64. package/explorer/src/workspaces/GraphWorkspace/scene.ts +79 -0
  65. package/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts +421 -0
  66. package/explorer/src/workspaces/GraphWorkspace/smallGraphLayout.ts +135 -0
  67. package/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts +31 -0
  68. package/explorer/src/workspaces/GraphWorkspace/temporalScrubberBounds.ts +46 -0
  69. package/explorer/src/workspaces/GraphWorkspace/temporalSnapshotGuards.ts +113 -0
  70. package/explorer/src/workspaces/GraphWorkspace/types.ts +395 -0
  71. package/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +746 -0
  72. package/explorer/src/workspaces/GraphWorkspace/useMarkdownEditor.ts +177 -0
  73. package/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx +196 -0
  74. package/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +177 -0
  75. package/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +291 -0
  76. package/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx +346 -0
  77. package/explorer/src/workspaces/MemoryWorkspace.tsx +464 -0
  78. package/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +437 -0
  79. package/explorer/src/workspaces/OntologyWorkspace/HealthTab.tsx +214 -0
  80. package/explorer/src/workspaces/OntologyWorkspace/OntologyEditor.tsx +770 -0
  81. package/explorer/src/workspaces/OntologyWorkspace/OntologyLoader.tsx +913 -0
  82. package/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +930 -0
  83. package/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +590 -0
  84. package/explorer/src/workspaces/OntologyWorkspace/ProposalReview.tsx +429 -0
  85. package/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +696 -0
  86. package/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +363 -0
  87. package/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +546 -0
  88. package/explorer/src/workspaces/OntologyWorkspace/api.ts +164 -0
  89. package/explorer/src/workspaces/OntologyWorkspace/index.tsx +101 -0
  90. package/explorer/src/workspaces/OntologyWorkspace/ontologyEditorModel.ts +87 -0
  91. package/explorer/src/workspaces/OntologyWorkspace/ontologyUrlState.ts +94 -0
  92. package/explorer/src/workspaces/OntologyWorkspace/types.ts +119 -0
  93. package/explorer/src/workspaces/ReasoningWorkspace.tsx +243 -0
  94. package/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +271 -0
  95. package/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx +97 -0
  96. package/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx +95 -0
  97. package/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx +116 -0
  98. package/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx +112 -0
  99. package/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx +70 -0
  100. package/explorer/src/workspaces/VocabularyWorkspace/queries.ts +52 -0
  101. package/explorer/src/workspaces/VocabularyWorkspace/types.ts +24 -0
  102. package/package.json +31 -0
  103. package/plugins/.claude-plugin/README.md +262 -0
  104. package/plugins/.claude-plugin/marketplace.json +20 -0
  105. package/plugins/.claude-plugin/plugin.json +33 -0
  106. package/plugins/.cline-plugin/README.md +32 -0
  107. package/plugins/.cline-plugin/marketplace.json +17 -0
  108. package/plugins/.cline-plugin/plugin.json +35 -0
  109. package/plugins/.codex-plugin/marketplace.json +21 -0
  110. package/plugins/.codex-plugin/plugin.json +35 -0
  111. package/plugins/.continue-plugin/README.md +34 -0
  112. package/plugins/.continue-plugin/marketplace.json +17 -0
  113. package/plugins/.continue-plugin/plugin.json +33 -0
  114. package/plugins/.cursor-plugin/marketplace.json +18 -0
  115. package/plugins/.cursor-plugin/plugin.json +32 -0
  116. package/plugins/.openclaw-plugin/README.md +62 -0
  117. package/plugins/.openclaw-plugin/marketplace.json +18 -0
  118. package/plugins/.openclaw-plugin/plugin.json +46 -0
  119. package/plugins/.pi-plugin/README.md +35 -0
  120. package/plugins/.vscode-plugin/README.md +48 -0
  121. package/plugins/.vscode-plugin/marketplace.json +17 -0
  122. package/plugins/.vscode-plugin/plugin.json +35 -0
  123. package/plugins/.windsurf-plugin/README.md +39 -0
  124. package/plugins/.windsurf-plugin/marketplace.json +17 -0
  125. package/plugins/.windsurf-plugin/plugin.json +35 -0
  126. package/plugins/agents/decision-advisor.md +126 -0
  127. package/plugins/agents/explainability.md +129 -0
  128. package/plugins/agents/kg-assistant.md +73 -0
  129. package/plugins/hooks/hooks.json +12 -0
  130. package/plugins/skills/causal/SKILL.md +74 -0
  131. package/plugins/skills/change/SKILL.md +80 -0
  132. package/plugins/skills/decision/SKILL.md +197 -0
  133. package/plugins/skills/deduplicate/SKILL.md +41 -0
  134. package/plugins/skills/embed/SKILL.md +230 -0
  135. package/plugins/skills/explain/SKILL.md +44 -0
  136. package/plugins/skills/export/SKILL.md +67 -0
  137. package/plugins/skills/extract/SKILL.md +93 -0
  138. package/plugins/skills/ingest/SKILL.md +38 -0
  139. package/plugins/skills/ontology/SKILL.md +89 -0
  140. package/plugins/skills/policy/SKILL.md +66 -0
  141. package/plugins/skills/provenance/SKILL.md +70 -0
  142. package/plugins/skills/query/SKILL.md +84 -0
  143. package/plugins/skills/reason/SKILL.md +201 -0
  144. package/plugins/skills/temporal/SKILL.md +164 -0
  145. package/plugins/skills/validate/SKILL.md +228 -0
  146. package/plugins/skills/visualize/SKILL.md +249 -0
@@ -0,0 +1,437 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import type { CSSProperties } from "react";
3
+ import { GitMerge, Loader2, Sparkles, Trash2 } from "lucide-react";
4
+ import {
5
+ loadAlignments,
6
+ loadOntologyRegistry,
7
+ removeAlignment,
8
+ saveAlignment,
9
+ suggestAlignments,
10
+ } from "./api";
11
+ import type { AlignmentRelation, AlignmentSuggestion, OntologyAlignment, OntologyEntry } from "./types";
12
+
13
+ const RELATIONS: AlignmentRelation[] = [
14
+ "owl:equivalentClass",
15
+ "owl:equivalentProperty",
16
+ "skos:exactMatch",
17
+ "skos:closeMatch",
18
+ "skos:broadMatch",
19
+ "skos:narrowMatch",
20
+ "skos:relatedMatch",
21
+ ];
22
+
23
+ const RELATION_COLORS: Record<AlignmentRelation, string> = {
24
+ "owl:equivalentClass": "#7ce7d3",
25
+ "owl:equivalentProperty": "#7ce7d3",
26
+ "skos:exactMatch": "#9ee8d7",
27
+ "skos:closeMatch": "#ff2d4b",
28
+ "skos:broadMatch": "#ff7a45",
29
+ "skos:narrowMatch": "#ff7a45",
30
+ "skos:relatedMatch": "#d2a8ff",
31
+ };
32
+
33
+ export function AlignmentsTab() {
34
+ const [registry, setRegistry] = useState<OntologyEntry[]>([]);
35
+ const [alignments, setAlignments] = useState<OntologyAlignment[]>([]);
36
+ const [suggestions, setSuggestions] = useState<AlignmentSuggestion[]>([]);
37
+ const [sourceOntology, setSourceOntology] = useState("");
38
+ const [targetOntology, setTargetOntology] = useState("");
39
+ const [sourceUri, setSourceUri] = useState("");
40
+ const [targetUri, setTargetUri] = useState("");
41
+ const [relation, setRelation] = useState<AlignmentRelation>("skos:exactMatch");
42
+ const [confidence, setConfidence] = useState(0.86);
43
+ const [provenance, setProvenance] = useState("");
44
+ const [source, setSource] = useState("Ontology Hub");
45
+ const [reviewer, setReviewer] = useState("");
46
+ const [threshold, setThreshold] = useState(0.68);
47
+ const [busy, setBusy] = useState(false);
48
+ const [error, setError] = useState("");
49
+
50
+ const reload = useCallback(async () => {
51
+ setError("");
52
+ const [registryResult, alignmentResult] = await Promise.allSettled([
53
+ loadOntologyRegistry(),
54
+ loadAlignments(),
55
+ ]);
56
+
57
+ const errors: string[] = [];
58
+ if (registryResult.status === "fulfilled") {
59
+ setRegistry(registryResult.value);
60
+ setSourceOntology((current) => current || registryResult.value[0]?.uri || "");
61
+ setTargetOntology((current) => current || registryResult.value[1]?.uri || registryResult.value[0]?.uri || "");
62
+ } else {
63
+ errors.push(registryResult.reason instanceof Error ? registryResult.reason.message : "Failed to load ontology registry.");
64
+ }
65
+ if (alignmentResult.status === "fulfilled") {
66
+ setAlignments(alignmentResult.value);
67
+ } else {
68
+ errors.push(alignmentResult.reason instanceof Error ? alignmentResult.reason.message : "Failed to load alignments.");
69
+ }
70
+ if (errors.length) setError(errors.join(" "));
71
+ }, []);
72
+
73
+ useEffect(() => {
74
+ let ignore = false;
75
+ async function fetchInitial() {
76
+ const [registryResult, alignmentResult] = await Promise.allSettled([
77
+ loadOntologyRegistry(),
78
+ loadAlignments(),
79
+ ]);
80
+ if (ignore) return;
81
+
82
+ const errors: string[] = [];
83
+ if (registryResult.status === "fulfilled") {
84
+ setRegistry(registryResult.value);
85
+ setSourceOntology((current) => current || registryResult.value[0]?.uri || "");
86
+ setTargetOntology((current) => current || registryResult.value[1]?.uri || registryResult.value[0]?.uri || "");
87
+ } else {
88
+ errors.push(registryResult.reason instanceof Error ? registryResult.reason.message : "Failed to load ontology registry.");
89
+ }
90
+ if (alignmentResult.status === "fulfilled") {
91
+ setAlignments(alignmentResult.value);
92
+ } else {
93
+ errors.push(alignmentResult.reason instanceof Error ? alignmentResult.reason.message : "Failed to load alignments.");
94
+ }
95
+ if (errors.length) setError(errors.join(" "));
96
+ }
97
+ void fetchInitial();
98
+ return () => { ignore = true; };
99
+ }, []);
100
+
101
+ const relationCounts = useMemo(() => {
102
+ const counts = new Map<string, number>();
103
+ for (const item of alignments) {
104
+ counts.set(item.relation, (counts.get(item.relation) ?? 0) + 1);
105
+ }
106
+ return counts;
107
+ }, [alignments]);
108
+
109
+ // Pairwise matrix: group alignments by (source_ontology, target_ontology) pair.
110
+ const matrix = useMemo(() => {
111
+ function ontologyOfUri(uri: string): string {
112
+ for (const entry of registry) {
113
+ if (uri === entry.uri || uri.startsWith(entry.uri + "#") || uri.startsWith(entry.uri + "/")) {
114
+ return entry.uri;
115
+ }
116
+ }
117
+ const hashIdx = uri.lastIndexOf("#");
118
+ if (hashIdx > 0) return uri.substring(0, hashIdx);
119
+ const slashIdx = uri.lastIndexOf("/");
120
+ return slashIdx > 0 ? uri.substring(0, slashIdx) : uri;
121
+ }
122
+ const cells: Map<string, OntologyAlignment[]> = new Map();
123
+ for (const alignment of alignments) {
124
+ const key = `${ontologyOfUri(alignment.source_uri)}|||${ontologyOfUri(alignment.target_uri)}`;
125
+ const bucket = cells.get(key) ?? [];
126
+ bucket.push(alignment);
127
+ cells.set(key, bucket);
128
+ }
129
+ return { ontologies: registry, cells };
130
+ }, [registry, alignments]);
131
+
132
+ const handleSave = useCallback(async () => {
133
+ if (!sourceUri.trim() || !targetUri.trim()) {
134
+ setError("Provide both source and target entity URIs.");
135
+ return;
136
+ }
137
+ setBusy(true);
138
+ setError("");
139
+ try {
140
+ await saveAlignment({
141
+ source_uri: sourceUri.trim(),
142
+ target_uri: targetUri.trim(),
143
+ relation,
144
+ confidence,
145
+ provenance: provenance || undefined,
146
+ source: source || undefined,
147
+ reviewer: reviewer || undefined,
148
+ });
149
+ setSourceUri("");
150
+ setTargetUri("");
151
+ await reload();
152
+ } catch (err) {
153
+ setError(err instanceof Error ? err.message : "Could not save alignment.");
154
+ } finally {
155
+ setBusy(false);
156
+ }
157
+ }, [sourceUri, targetUri, relation, confidence, provenance, source, reviewer, reload]);
158
+
159
+ const handleSuggest = useCallback(async () => {
160
+ setBusy(true);
161
+ setError("");
162
+ try {
163
+ const data = await suggestAlignments({
164
+ source_ontology_uri: sourceOntology || undefined,
165
+ target_ontology_uri: targetOntology || undefined,
166
+ threshold,
167
+ limit: 40,
168
+ });
169
+ setSuggestions(data);
170
+ } catch (err) {
171
+ setError(err instanceof Error ? err.message : "Could not suggest alignments.");
172
+ } finally {
173
+ setBusy(false);
174
+ }
175
+ }, [sourceOntology, targetOntology, threshold]);
176
+
177
+ const handleAcceptSuggestion = useCallback((suggestion: AlignmentSuggestion) => {
178
+ setSourceUri(suggestion.source_uri);
179
+ setTargetUri(suggestion.target_uri);
180
+ setRelation(suggestion.relation);
181
+ setConfidence(Math.max(0.1, Math.min(1, suggestion.score)));
182
+ setProvenance(suggestion.reason);
183
+ }, []);
184
+
185
+ const handleRemove = useCallback(async (id: string) => {
186
+ setBusy(true);
187
+ setError("");
188
+ try {
189
+ await removeAlignment(id);
190
+ await reload();
191
+ } catch (err) {
192
+ setError(err instanceof Error ? err.message : "Could not remove alignment.");
193
+ } finally {
194
+ setBusy(false);
195
+ }
196
+ }, [reload]);
197
+
198
+ return (
199
+ <div style={pageStyle}>
200
+ <section style={heroStyle}>
201
+ <div>
202
+ <div style={kickerStyle}><GitMerge size={14} /> Alignment Matrix</div>
203
+ <h2 style={titleStyle}>Cross-ontology mappings</h2>
204
+ <p style={textStyle}>
205
+ Manage equivalence and SKOS match relations with confidence, provenance,
206
+ reviewer context, and label-based suggestions.
207
+ </p>
208
+ </div>
209
+ <div style={summaryGridStyle}>
210
+ <Metric label="Mappings" value={alignments.length} />
211
+ <Metric label="Relations" value={relationCounts.size} />
212
+ <Metric label="Suggestions" value={suggestions.length} />
213
+ </div>
214
+ </section>
215
+
216
+ {error ? <div style={errorStyle}>{error}</div> : null}
217
+
218
+ <div style={ephemeralBannerStyle}>
219
+ Alignments are stored in server memory and are not persisted across restarts.
220
+ Export your graph or ontology to preserve recorded mappings.
221
+ </div>
222
+
223
+ {matrix.ontologies.length >= 2 ? (
224
+ <section style={cardStyle}>
225
+ <h3 style={sectionTitleStyle}>Pairwise alignment matrix</h3>
226
+ <div style={{ overflowX: "auto" }}>
227
+ <table style={matrixTableStyle}>
228
+ <thead>
229
+ <tr>
230
+ <th style={matrixCornerStyle} />
231
+ {matrix.ontologies.map((col) => (
232
+ <th key={col.uri} style={matrixColHeaderStyle}>{col.name}</th>
233
+ ))}
234
+ </tr>
235
+ </thead>
236
+ <tbody>
237
+ {matrix.ontologies.map((row) => (
238
+ <tr key={row.uri}>
239
+ <td style={matrixRowHeaderStyle}>{row.name}</td>
240
+ {matrix.ontologies.map((col) => {
241
+ const key = `${row.uri}|||${col.uri}`;
242
+ const cellItems = matrix.cells.get(key) ?? [];
243
+ const isDiag = row.uri === col.uri;
244
+ return (
245
+ <td key={col.uri} style={{ ...matrixCellStyle, background: isDiag ? "rgba(255,255,255,0.015)" : undefined }}>
246
+ {isDiag ? <span style={mutedStyle}>—</span> : cellItems.length ? (
247
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
248
+ {cellItems.map((item) => (
249
+ <span
250
+ key={item.id}
251
+ style={{ ...relationBadgeStyle, color: RELATION_COLORS[item.relation], borderColor: `${RELATION_COLORS[item.relation]}44`, cursor: "pointer", fontSize: 9 }}
252
+ title={`${item.source_label} → ${item.target_label} (${Math.round(item.confidence * 100)}%)`}
253
+ onClick={() => {
254
+ setSourceUri(item.source_uri);
255
+ setTargetUri(item.target_uri);
256
+ setRelation(item.relation);
257
+ setConfidence(item.confidence);
258
+ setProvenance(item.provenance ?? "");
259
+ }}
260
+ >
261
+ {item.relation.split(":")[1]}
262
+ </span>
263
+ ))}
264
+ </div>
265
+ ) : <span style={{ color: "rgba(255,107,129,0.15)", fontSize: 12 }}>·</span>}
266
+ </td>
267
+ );
268
+ })}
269
+ </tr>
270
+ ))}
271
+ </tbody>
272
+ </table>
273
+ </div>
274
+ <p style={{ ...mutedStyle, marginTop: 10 }}>Click a relation badge to load it into the editor below.</p>
275
+ </section>
276
+ ) : null}
277
+
278
+ <div style={gridStyle}>
279
+ <section style={cardStyle}>
280
+ <h3 style={sectionTitleStyle}>Create or update alignment</h3>
281
+ <label style={labelStyle}>Source entity URI</label>
282
+ <input style={inputStyle} value={sourceUri} onChange={(event) => setSourceUri(event.target.value)} />
283
+ <label style={labelStyle}>Target entity URI</label>
284
+ <input style={inputStyle} value={targetUri} onChange={(event) => setTargetUri(event.target.value)} />
285
+ <div style={twoColStyle}>
286
+ <div>
287
+ <label style={labelStyle}>Relation</label>
288
+ <select style={inputStyle} value={relation} onChange={(event) => setRelation(event.target.value as AlignmentRelation)}>
289
+ {RELATIONS.map((item) => <option key={item}>{item}</option>)}
290
+ </select>
291
+ </div>
292
+ <div>
293
+ <label style={labelStyle}>Confidence {confidence.toFixed(2)}</label>
294
+ <input
295
+ type="range"
296
+ min="0"
297
+ max="1"
298
+ step="0.01"
299
+ value={confidence}
300
+ onChange={(event) => setConfidence(Number(event.target.value))}
301
+ style={{ width: "100%" }}
302
+ />
303
+ </div>
304
+ </div>
305
+ <label style={labelStyle}>Provenance note</label>
306
+ <textarea style={{ ...inputStyle, minHeight: 74, resize: "vertical" }} value={provenance} onChange={(event) => setProvenance(event.target.value)} />
307
+ <div style={twoColStyle}>
308
+ <div>
309
+ <label style={labelStyle}>Source</label>
310
+ <input style={inputStyle} value={source} onChange={(event) => setSource(event.target.value)} />
311
+ </div>
312
+ <div>
313
+ <label style={labelStyle}>Reviewer</label>
314
+ <input style={inputStyle} value={reviewer} onChange={(event) => setReviewer(event.target.value)} />
315
+ </div>
316
+ </div>
317
+ <button style={primaryButtonStyle} disabled={busy} onClick={handleSave}>
318
+ {busy ? <Loader2 size={14} className="ws-spin" /> : <GitMerge size={14} />}
319
+ Save alignment
320
+ </button>
321
+ </section>
322
+
323
+ <section style={cardStyle}>
324
+ <h3 style={sectionTitleStyle}>Suggest alignments</h3>
325
+ <div style={twoColStyle}>
326
+ <div>
327
+ <label style={labelStyle}>Source ontology</label>
328
+ <select style={inputStyle} value={sourceOntology} onChange={(event) => setSourceOntology(event.target.value)}>
329
+ <option value="">Any ontology</option>
330
+ {registry.map((entry) => <option key={entry.uri} value={entry.uri}>{entry.name}</option>)}
331
+ </select>
332
+ </div>
333
+ <div>
334
+ <label style={labelStyle}>Target ontology</label>
335
+ <select style={inputStyle} value={targetOntology} onChange={(event) => setTargetOntology(event.target.value)}>
336
+ <option value="">Any ontology</option>
337
+ {registry.map((entry) => <option key={entry.uri} value={entry.uri}>{entry.name}</option>)}
338
+ </select>
339
+ </div>
340
+ </div>
341
+ <label style={labelStyle}>Similarity threshold {threshold.toFixed(2)}</label>
342
+ <input
343
+ type="range"
344
+ min="0.25"
345
+ max="0.95"
346
+ step="0.01"
347
+ value={threshold}
348
+ onChange={(event) => setThreshold(Number(event.target.value))}
349
+ style={{ width: "100%" }}
350
+ />
351
+ <button style={secondaryButtonStyle} disabled={busy} onClick={handleSuggest}>
352
+ <Sparkles size={14} />
353
+ Suggest alignments
354
+ </button>
355
+ <div style={suggestionListStyle}>
356
+ {suggestions.map((item) => (
357
+ <button key={`${item.source_uri}-${item.target_uri}-${item.relation}`} style={suggestionStyle} onClick={() => handleAcceptSuggestion(item)}>
358
+ <span style={{ color: "#fff0f2", fontWeight: 800 }}>{item.source_label}</span>
359
+ <span style={{ color: RELATION_COLORS[item.relation] }}>{item.relation}</span>
360
+ <span style={{ color: "#fff0f2", fontWeight: 800 }}>{item.target_label}</span>
361
+ <span style={{ color: "#c99aa3" }}>{Math.round(item.score * 100)}%</span>
362
+ </button>
363
+ ))}
364
+ {!suggestions.length ? <p style={mutedStyle}>Run suggestions to review ranked candidate mappings.</p> : null}
365
+ </div>
366
+ </section>
367
+ </div>
368
+
369
+ <section style={cardStyle}>
370
+ <h3 style={sectionTitleStyle}>Recorded alignments</h3>
371
+ <div style={tableStyle}>
372
+ {alignments.map((item) => (
373
+ <div key={item.id} style={rowStyle}>
374
+ <div>
375
+ <div style={{ color: "#fff0f2", fontWeight: 800 }}>{item.source_label || item.source_uri}</div>
376
+ <div style={monoStyle}>{item.source_uri}</div>
377
+ </div>
378
+ <div style={{ ...relationBadgeStyle, color: RELATION_COLORS[item.relation], borderColor: `${RELATION_COLORS[item.relation]}55` }}>
379
+ {item.relation}
380
+ </div>
381
+ <div>
382
+ <div style={{ color: "#fff0f2", fontWeight: 800 }}>{item.target_label || item.target_uri}</div>
383
+ <div style={monoStyle}>{item.target_uri}</div>
384
+ </div>
385
+ <div style={confidenceStyle}>{Math.round(item.confidence * 100)}%</div>
386
+ <button style={iconButtonStyle} disabled={busy} onClick={() => handleRemove(item.id)} title="Remove alignment">
387
+ <Trash2 size={14} />
388
+ </button>
389
+ </div>
390
+ ))}
391
+ {!alignments.length ? <p style={mutedStyle}>No alignments recorded yet.</p> : null}
392
+ </div>
393
+ </section>
394
+ </div>
395
+ );
396
+ }
397
+
398
+ function Metric({ label, value }: { label: string; value: number }) {
399
+ return (
400
+ <div style={metricStyle}>
401
+ <span style={{ color: "#9ee8d7", fontSize: 20, fontWeight: 900 }}>{value.toLocaleString()}</span>
402
+ <span style={{ color: "#9c7a82", fontSize: 11 }}>{label}</span>
403
+ </div>
404
+ );
405
+ }
406
+
407
+ const pageStyle: CSSProperties = { height: "100%", overflow: "auto", padding: 22, display: "flex", flexDirection: "column", gap: 16 };
408
+ const heroStyle: CSSProperties = { display: "flex", justifyContent: "space-between", gap: 18, padding: 22, border: "1px solid rgba(255,107,129,0.12)", borderRadius: 22, background: "linear-gradient(135deg, rgba(11,25,42,0.94), rgba(7,14,25,0.9))" };
409
+ const kickerStyle: CSSProperties = { display: "inline-flex", gap: 8, alignItems: "center", color: "#9ee8d7", fontSize: 11, fontWeight: 900, letterSpacing: "0.12em", textTransform: "uppercase" };
410
+ const titleStyle: CSSProperties = { margin: "8px 0", color: "#fff0f2", fontSize: 26, letterSpacing: "-0.04em" };
411
+ const textStyle: CSSProperties = { margin: 0, color: "#c99aa3", lineHeight: 1.6, maxWidth: 620 };
412
+ const summaryGridStyle: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(3, minmax(100px, 1fr))", gap: 10, minWidth: 320 };
413
+ const metricStyle: CSSProperties = { padding: 14, borderRadius: 16, background: "rgba(255,255,255,0.035)", border: "1px solid rgba(255,107,129,0.1)", display: "flex", flexDirection: "column", gap: 4 };
414
+ const gridStyle: CSSProperties = { display: "grid", gridTemplateColumns: "minmax(320px, 0.9fr) minmax(360px, 1.1fr)", gap: 16 };
415
+ const cardStyle: CSSProperties = { padding: 18, border: "1px solid rgba(255,107,129,0.12)", borderRadius: 20, background: "rgba(12,6,9,0.78)", boxShadow: "inset 0 1px 0 rgba(255,255,255,0.04)" };
416
+ const sectionTitleStyle: CSSProperties = { margin: "0 0 14px", color: "#fff0f2", fontSize: 16 };
417
+ const labelStyle: CSSProperties = { display: "block", color: "#9c7a82", fontSize: 11, fontWeight: 800, margin: "10px 0 6px", textTransform: "uppercase", letterSpacing: "0.08em" };
418
+ const inputStyle: CSSProperties = { width: "100%", boxSizing: "border-box", border: "1px solid rgba(255,107,129,0.14)", borderRadius: 12, padding: "10px 12px", background: "rgba(6,3,5,0.8)", color: "#fff0f2" };
419
+ const twoColStyle: CSSProperties = { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 };
420
+ const primaryButtonStyle: CSSProperties = { marginTop: 14, width: "100%", border: "1px solid rgba(124,231,211,0.35)", borderRadius: 12, padding: "11px 13px", background: "linear-gradient(135deg, rgba(20,151,136,0.55), rgba(255,45,75,0.35))", color: "#fff0f2", fontWeight: 900, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8 };
421
+ const secondaryButtonStyle: CSSProperties = { ...primaryButtonStyle, background: "rgba(255,107,129,0.08)", borderColor: "rgba(255,107,129,0.18)" };
422
+ const suggestionListStyle: CSSProperties = { display: "flex", flexDirection: "column", gap: 8, marginTop: 14, maxHeight: 260, overflow: "auto" };
423
+ const suggestionStyle: CSSProperties = { display: "grid", gridTemplateColumns: "1fr auto 1fr auto", gap: 10, alignItems: "center", textAlign: "left", border: "1px solid rgba(255,107,129,0.1)", borderRadius: 12, background: "rgba(255,255,255,0.03)", padding: 10, cursor: "pointer" };
424
+ const tableStyle: CSSProperties = { display: "flex", flexDirection: "column", gap: 8 };
425
+ const rowStyle: CSSProperties = { display: "grid", gridTemplateColumns: "1fr auto 1fr auto auto", gap: 12, alignItems: "center", padding: 12, borderRadius: 14, background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,107,129,0.08)" };
426
+ const monoStyle: CSSProperties = { color: "#9c7a82", fontSize: 11, fontFamily: "JetBrains Mono, monospace", wordBreak: "break-all" };
427
+ const relationBadgeStyle: CSSProperties = { padding: "5px 9px", border: "1px solid", borderRadius: 999, fontSize: 10, fontWeight: 900 };
428
+ const confidenceStyle: CSSProperties = { color: "#ff7a45", fontWeight: 900 };
429
+ const iconButtonStyle: CSSProperties = { width: 34, height: 34, borderRadius: 10, border: "1px solid rgba(255,157,175,0.18)", background: "rgba(255,157,175,0.08)", color: "#ff9daf", cursor: "pointer" };
430
+ const mutedStyle: CSSProperties = { margin: 0, color: "#9c7a82", fontSize: 13 };
431
+ const errorStyle: CSSProperties = { padding: 12, borderRadius: 14, color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)" };
432
+ const ephemeralBannerStyle: CSSProperties = { padding: "9px 14px", borderRadius: 12, color: "#ff7a45", background: "rgba(242,182,109,0.08)", border: "1px solid rgba(242,182,109,0.22)", fontSize: 12 };
433
+ const matrixTableStyle: CSSProperties = { borderCollapse: "collapse", minWidth: "100%", fontSize: 12 };
434
+ const matrixCornerStyle: CSSProperties = { padding: "8px 12px", borderBottom: "1px solid rgba(255,107,129,0.1)" };
435
+ const matrixColHeaderStyle: CSSProperties = { padding: "8px 12px", color: "#9ee8d7", fontWeight: 900, borderBottom: "1px solid rgba(255,107,129,0.1)", textAlign: "center", whiteSpace: "nowrap" };
436
+ const matrixRowHeaderStyle: CSSProperties = { padding: "8px 12px", color: "#9ee8d7", fontWeight: 900, borderRight: "1px solid rgba(255,107,129,0.1)", whiteSpace: "nowrap" };
437
+ const matrixCellStyle: CSSProperties = { padding: "8px 10px", borderBottom: "1px solid rgba(255,107,129,0.06)", borderRight: "1px solid rgba(255,107,129,0.06)", textAlign: "center", verticalAlign: "middle", minWidth: 100 };
@@ -0,0 +1,214 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import type { CSSProperties } from "react";
3
+ import { Download, HeartPulse, Loader2, Wrench } from "lucide-react";
4
+ import { loadOntologyHealth, loadOntologyRegistry } from "./api";
5
+ import type { OntologyEntry, OntologyHealthResponse, HealthIssue } from "./types";
6
+
7
+ interface HealthTabProps {
8
+ onFixInEditor?: (entityUri: string) => void;
9
+ }
10
+
11
+ export function HealthTab({ onFixInEditor }: HealthTabProps) {
12
+ const [registry, setRegistry] = useState<OntologyEntry[]>([]);
13
+ const [selectedUri, setSelectedUri] = useState("");
14
+ const [health, setHealth] = useState<OntologyHealthResponse | null>(null);
15
+ const [loading, setLoading] = useState(false);
16
+ const [error, setError] = useState("");
17
+
18
+ useEffect(() => {
19
+ let cancelled = false;
20
+ loadOntologyRegistry()
21
+ .then((entries) => {
22
+ if (cancelled) return;
23
+ setRegistry(entries);
24
+ setSelectedUri((current) => current || entries[0]?.uri || "");
25
+ })
26
+ .catch((err) => {
27
+ if (cancelled) return;
28
+ setError(err instanceof Error ? err.message : "Failed to load ontology registry.");
29
+ });
30
+ return () => {
31
+ cancelled = true;
32
+ };
33
+ }, []);
34
+
35
+ const [prevUri, setPrevUri] = useState(selectedUri);
36
+ if (selectedUri !== prevUri) {
37
+ setPrevUri(selectedUri);
38
+ if (selectedUri) {
39
+ setLoading(true);
40
+ setError("");
41
+ }
42
+ }
43
+
44
+ useEffect(() => {
45
+ let ignore = false;
46
+ async function fetchHealth() {
47
+ if (!selectedUri) return;
48
+ try {
49
+ const data = await loadOntologyHealth(selectedUri);
50
+ if (!ignore) setHealth(data);
51
+ } catch {
52
+ if (!ignore) setHealth(null);
53
+ } finally {
54
+ if (!ignore) setLoading(false);
55
+ }
56
+ }
57
+ void fetchHealth();
58
+ return () => { ignore = true; };
59
+ }, [selectedUri]);
60
+
61
+ const exportReport = useCallback(() => {
62
+ if (!health) return;
63
+ const blob = new Blob([JSON.stringify(health, null, 2)], { type: "application/json" });
64
+ const url = URL.createObjectURL(blob);
65
+ const anchor = document.createElement("a");
66
+ anchor.href = url;
67
+ anchor.download = `${health.name.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}-health.json`;
68
+ document.body.appendChild(anchor);
69
+ anchor.click();
70
+ document.body.removeChild(anchor);
71
+ setTimeout(() => URL.revokeObjectURL(url), 100);
72
+ }, [health]);
73
+
74
+ return (
75
+ <div style={pageStyle}>
76
+ <section style={heroStyle}>
77
+ <div>
78
+ <div style={kickerStyle}><HeartPulse size={14} /> Ontology Health</div>
79
+ <h2 style={titleStyle}>Quality and governance signals</h2>
80
+ <p style={textStyle}>
81
+ Score completeness, consistency, SHACL readiness, alignment coverage,
82
+ and documentation quality for the selected ontology.
83
+ </p>
84
+ </div>
85
+ <div style={selectorShellStyle}>
86
+ <label style={labelStyle}>Ontology</label>
87
+ <select style={inputStyle} value={selectedUri} onChange={(event) => setSelectedUri(event.target.value)}>
88
+ {registry.map((entry) => <option key={entry.uri} value={entry.uri}>{entry.name}</option>)}
89
+ </select>
90
+ </div>
91
+ </section>
92
+
93
+ {error ? <div style={errorStyle}>{error}</div> : null}
94
+
95
+ {loading ? (
96
+ <div style={loadingStyle}><Loader2 size={18} className="ws-spin" /> Computing health dashboard...</div>
97
+ ) : health ? (
98
+ <>
99
+ <section style={{ ...scoreGridStyle, gridTemplateColumns: `220px repeat(${health.dimensions.length}, minmax(180px, 1fr))` }}>
100
+ <div style={scoreCardStyle}>
101
+ <span style={scoreValueStyle}>{Math.round(health.total_score)}</span>
102
+ <span style={mutedStyle}>Total health score</span>
103
+ <button style={secondaryButtonStyle} onClick={exportReport}><Download size={14} /> Export report</button>
104
+ </div>
105
+ {health.dimensions.map((dimension) => (
106
+ <div key={dimension.key} style={dimensionCardStyle}>
107
+ <div style={dimensionHeadStyle}>
108
+ <span style={{ color: "#fff0f2", fontWeight: 900 }}>{dimension.label}</span>
109
+ <span style={statusBadgeStyle(dimension.status)}>{dimension.status}</span>
110
+ </div>
111
+ <div style={barTrackStyle}>
112
+ <div style={{ ...barFillStyle, width: `${dimension.score}%`, background: dimensionColor(dimension.score, dimension.status) }} />
113
+ </div>
114
+ <div style={dimensionFootStyle}>
115
+ <span>{Math.round(dimension.score)} / 100</span>
116
+ <span>{dimension.detail}</span>
117
+ </div>
118
+ </div>
119
+ ))}
120
+ </section>
121
+
122
+ <section style={cardStyle}>
123
+ <h3 style={sectionTitleStyle}>Actionable issues</h3>
124
+ <div style={issueListStyle}>
125
+ {health.issues.map((issue) => (
126
+ <IssueRow key={issue.id} issue={issue} onFixInEditor={onFixInEditor} />
127
+ ))}
128
+ {!health.issues.length ? <p style={mutedStyle}>No actionable issues reported for this ontology.</p> : null}
129
+ </div>
130
+ </section>
131
+ </>
132
+ ) : (
133
+ <div style={emptyStyle}>Select an ontology to compute health signals.</div>
134
+ )}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function IssueRow({ issue, onFixInEditor }: { issue: HealthIssue; onFixInEditor?: (entityUri: string) => void }) {
140
+ return (
141
+ <div style={issueRowStyle}>
142
+ <div style={severityDotStyle(issue.severity)} />
143
+ <div>
144
+ <div style={{ color: "#fff0f2", fontWeight: 800 }}>{issue.entity_label || issue.category}</div>
145
+ <div style={{ color: "#c99aa3", fontSize: 13, lineHeight: 1.45 }}>{issue.message}</div>
146
+ {issue.entity_uri ? <div style={monoStyle}>{issue.entity_uri}</div> : null}
147
+ </div>
148
+ <span style={categoryStyle}>{issue.category}</span>
149
+ {issue.entity_uri ? (
150
+ <button style={smallButtonStyle} onClick={() => onFixInEditor?.(issue.entity_uri || "")}>
151
+ <Wrench size={13} />
152
+ Fix in Editor
153
+ </button>
154
+ ) : (
155
+ <div />
156
+ )}
157
+ </div>
158
+ );
159
+ }
160
+
161
+ function dimensionColor(score: number, status: string) {
162
+ if (status === "unavailable") return "#9c7a82";
163
+ if (score >= 80) return "#7ce7d3";
164
+ if (score >= 55) return "#ff7a45";
165
+ return "#ff9daf";
166
+ }
167
+
168
+ function statusBadgeStyle(status: string): CSSProperties {
169
+ const color = status === "ok" ? "#7ce7d3" : status === "unavailable" ? "#9c7a82" : "#ff7a45";
170
+ return {
171
+ color,
172
+ background: `${color}18`,
173
+ border: `1px solid ${color}30`,
174
+ borderRadius: 999,
175
+ padding: "2px 7px",
176
+ fontSize: 10,
177
+ fontWeight: 900,
178
+ textTransform: "uppercase",
179
+ };
180
+ }
181
+
182
+ function severityDotStyle(severity: string): CSSProperties {
183
+ const color = severity === "critical" ? "#ff9daf" : severity === "warning" ? "#ff7a45" : "#ff2d4b";
184
+ return { width: 10, height: 10, borderRadius: "50%", background: color, boxShadow: `0 0 18px ${color}55`, marginTop: 5 };
185
+ }
186
+
187
+ const pageStyle: CSSProperties = { height: "100%", overflow: "auto", padding: 22, display: "flex", flexDirection: "column", gap: 16 };
188
+ const heroStyle: CSSProperties = { display: "flex", justifyContent: "space-between", gap: 18, padding: 22, border: "1px solid rgba(255,107,129,0.12)", borderRadius: 22, background: "linear-gradient(135deg, rgba(11,25,42,0.94), rgba(7,14,25,0.9))" };
189
+ const kickerStyle: CSSProperties = { display: "inline-flex", gap: 8, alignItems: "center", color: "#9ee8d7", fontSize: 11, fontWeight: 900, letterSpacing: "0.12em", textTransform: "uppercase" };
190
+ const titleStyle: CSSProperties = { margin: "8px 0", color: "#fff0f2", fontSize: 26, letterSpacing: "-0.04em" };
191
+ const textStyle: CSSProperties = { margin: 0, color: "#c99aa3", lineHeight: 1.6, maxWidth: 620 };
192
+ const selectorShellStyle: CSSProperties = { minWidth: 320 };
193
+ const labelStyle: CSSProperties = { display: "block", color: "#9c7a82", fontSize: 11, fontWeight: 800, margin: "0 0 6px", textTransform: "uppercase", letterSpacing: "0.08em" };
194
+ const inputStyle: CSSProperties = { width: "100%", boxSizing: "border-box", border: "1px solid rgba(255,107,129,0.14)", borderRadius: 12, padding: "10px 12px", background: "rgba(6,3,5,0.8)", color: "#fff0f2" };
195
+ const scoreGridStyle: CSSProperties = { display: "grid", gridTemplateColumns: "220px repeat(5, minmax(180px, 1fr))", gap: 12 };
196
+ const scoreCardStyle: CSSProperties = { padding: 18, borderRadius: 20, background: "rgba(15,35,52,0.88)", border: "1px solid rgba(124,231,211,0.2)", display: "flex", flexDirection: "column", gap: 10 };
197
+ const scoreValueStyle: CSSProperties = { color: "#9ee8d7", fontSize: 52, lineHeight: 1, fontWeight: 950, letterSpacing: "-0.06em" };
198
+ const dimensionCardStyle: CSSProperties = { padding: 16, borderRadius: 18, background: "rgba(12,6,9,0.78)", border: "1px solid rgba(255,107,129,0.12)" };
199
+ const dimensionHeadStyle: CSSProperties = { display: "flex", justifyContent: "space-between", gap: 8, alignItems: "center", marginBottom: 12 };
200
+ const barTrackStyle: CSSProperties = { height: 8, borderRadius: 999, background: "rgba(255,255,255,0.06)", overflow: "hidden" };
201
+ const barFillStyle: CSSProperties = { height: "100%", borderRadius: 999 };
202
+ const dimensionFootStyle: CSSProperties = { display: "flex", flexDirection: "column", gap: 6, color: "#c99aa3", fontSize: 12, marginTop: 10 };
203
+ const cardStyle: CSSProperties = { padding: 18, border: "1px solid rgba(255,107,129,0.12)", borderRadius: 20, background: "rgba(12,6,9,0.78)" };
204
+ const sectionTitleStyle: CSSProperties = { margin: "0 0 14px", color: "#fff0f2", fontSize: 16 };
205
+ const issueListStyle: CSSProperties = { display: "flex", flexDirection: "column", gap: 8 };
206
+ const issueRowStyle: CSSProperties = { display: "grid", gridTemplateColumns: "14px 1fr auto auto", gap: 12, alignItems: "start", padding: 12, borderRadius: 14, background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,107,129,0.08)" };
207
+ const categoryStyle: CSSProperties = { color: "#9ee8d7", border: "1px solid rgba(158,232,215,0.22)", borderRadius: 999, padding: "4px 8px", fontSize: 10, fontWeight: 900 };
208
+ const smallButtonStyle: CSSProperties = { display: "inline-flex", gap: 6, alignItems: "center", border: "1px solid rgba(255,107,129,0.16)", borderRadius: 10, padding: "7px 9px", background: "rgba(255,107,129,0.08)", color: "#fff0f2", cursor: "pointer", fontWeight: 800 };
209
+ const secondaryButtonStyle: CSSProperties = { display: "inline-flex", gap: 8, alignItems: "center", justifyContent: "center", border: "1px solid rgba(255,107,129,0.16)", borderRadius: 12, padding: "10px 12px", background: "rgba(255,107,129,0.08)", color: "#fff0f2", cursor: "pointer", fontWeight: 900 };
210
+ const monoStyle: CSSProperties = { marginTop: 4, color: "#9c7a82", fontSize: 11, fontFamily: "JetBrains Mono, monospace", wordBreak: "break-all" };
211
+ const mutedStyle: CSSProperties = { margin: 0, color: "#9c7a82", fontSize: 13 };
212
+ const loadingStyle: CSSProperties = { display: "inline-flex", alignItems: "center", gap: 8, color: "#c99aa3", padding: 18 };
213
+ const emptyStyle: CSSProperties = { color: "#9c7a82", padding: 22 };
214
+ const errorStyle: CSSProperties = { display: "flex", alignItems: "center", gap: 8, padding: 12, borderRadius: 14, color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)" };