@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,696 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ AlertCircle,
4
+ BookOpen,
5
+ ChevronDown,
6
+ ChevronRight,
7
+ Loader2,
8
+ Search,
9
+ X,
10
+ } from "lucide-react";
11
+
12
+ interface SKOSScheme {
13
+ uri: string;
14
+ title: string;
15
+ description?: string;
16
+ concept_count: number;
17
+ }
18
+
19
+ interface ConceptNode {
20
+ uri: string;
21
+ pref_label: string;
22
+ alt_labels?: string[];
23
+ description?: string;
24
+ notation?: string;
25
+ scheme_uri?: string;
26
+ parent_uri?: string;
27
+ children?: ConceptNode[];
28
+ }
29
+
30
+ interface SKOSConceptDetail {
31
+ uri: string;
32
+ pref_label: string;
33
+ alt_labels: string[];
34
+ hidden_labels: string[];
35
+ definition?: string;
36
+ scope_note?: string;
37
+ editorial_note?: string;
38
+ broader: string[];
39
+ narrower: string[];
40
+ related: string[];
41
+ exact_match: string[];
42
+ close_match: string[];
43
+ broad_match: string[];
44
+ narrow_match: string[];
45
+ scheme_uri?: string;
46
+ }
47
+
48
+ function countConcepts(nodes: ConceptNode[]): number {
49
+ return nodes.reduce((acc, n) => acc + 1 + countConcepts(n.children ?? []), 0);
50
+ }
51
+
52
+ function LabelChip({ label }: { label: string }) {
53
+ return (
54
+ <span style={chipStyle}>{label}</span>
55
+ );
56
+ }
57
+
58
+ function UriLink({ uri }: { uri: string }) {
59
+ const short = uri.includes("#") ? uri.split("#").pop() : uri.split("/").pop();
60
+ return (
61
+ <span title={uri} style={{ color: "#ff2d4b", fontSize: 11, fontFamily: "monospace", cursor: "help" }}>
62
+ {short || uri}
63
+ </span>
64
+ );
65
+ }
66
+
67
+ function ConceptDetailPanel({
68
+ uri,
69
+ onClose,
70
+ onNavigate,
71
+ }: {
72
+ uri: string;
73
+ onClose: () => void;
74
+ onNavigate: (uri: string) => void;
75
+ }) {
76
+ const [detail, setDetail] = useState<SKOSConceptDetail | null>(null);
77
+ const [loading, setLoading] = useState(true);
78
+ const [error, setError] = useState("");
79
+
80
+ const [prevUri, setPrevUri] = useState(uri);
81
+ if (uri !== prevUri) {
82
+ setPrevUri(uri);
83
+ setLoading(true);
84
+ setError("");
85
+ setDetail(null);
86
+ }
87
+
88
+ useEffect(() => {
89
+ let ignore = false;
90
+ fetch(`/api/ontology/skos/concept/${encodeURIComponent(uri)}`)
91
+ .then(async (r) => {
92
+ if (!r.ok) throw new Error("Concept not found");
93
+ const data = await r.json();
94
+ if (r.status === 207) setError(data.message || "Warning: Partial success loading concept.");
95
+ return data;
96
+ })
97
+ .then((data) => {
98
+ if (!ignore) setDetail(data);
99
+ })
100
+ .catch((e) => {
101
+ if (!ignore) setError(e.message);
102
+ })
103
+ .finally(() => {
104
+ if (!ignore) setLoading(false);
105
+ });
106
+ return () => {
107
+ ignore = true;
108
+ };
109
+ }, [uri]);
110
+
111
+ const renderUriList = (label: string, uris: string[]) => {
112
+ if (!uris.length) return null;
113
+ return (
114
+ <PropSection label={label}>
115
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
116
+ {uris.map((u) => (
117
+ <button
118
+ key={u}
119
+ onClick={() => onNavigate(u)}
120
+ style={navLinkStyle}
121
+ >
122
+ <ChevronRight size={10} />
123
+ <UriLink uri={u} />
124
+ </button>
125
+ ))}
126
+ </div>
127
+ </PropSection>
128
+ );
129
+ };
130
+
131
+ return (
132
+ <div style={detailPanelStyle}>
133
+ <div style={detailHeaderStyle}>
134
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
135
+ <BookOpen size={13} color="#9ee8d7" />
136
+ <span style={{ color: "#fff0f2", fontSize: 13, fontWeight: 700 }}>Concept Detail</span>
137
+ </div>
138
+ <button onClick={onClose} style={iconBtnStyle}>
139
+ <X size={14} />
140
+ </button>
141
+ </div>
142
+
143
+ {loading && (
144
+ <div style={centerStyle}>
145
+ <Loader2 size={18} color="#ff2d4b" style={{ animation: "spin 1s linear infinite" }} />
146
+ </div>
147
+ )}
148
+
149
+ {error && (
150
+ <div style={centerStyle}>
151
+ <AlertCircle size={16} color="#ff9daf" />
152
+ <span style={{ color: "#ff9daf", fontSize: 12, marginTop: 6 }}>{error}</span>
153
+ </div>
154
+ )}
155
+
156
+ {detail && !loading && (
157
+ <div style={detailBodyStyle}>
158
+ <div style={{ marginBottom: 14 }}>
159
+ <h3 style={{ margin: "0 0 4px", color: "#fff0f2", fontSize: 17, fontWeight: 800, letterSpacing: "-0.03em" }}>
160
+ {detail.pref_label}
161
+ </h3>
162
+ {detail.alt_labels.length > 0 && (
163
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 6 }}>
164
+ {detail.alt_labels.map((l) => <LabelChip key={l} label={l} />)}
165
+ </div>
166
+ )}
167
+ {detail.hidden_labels.length > 0 && (
168
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 6 }}>
169
+ {detail.hidden_labels.map((l) => (
170
+ <span key={l} style={{ ...chipStyle, opacity: 0.5, fontStyle: "italic" }}>{l}</span>
171
+ ))}
172
+ </div>
173
+ )}
174
+ <div style={{ color: "#8f6d75", fontSize: 10, fontFamily: "monospace", wordBreak: "break-all" }}>
175
+ {uri}
176
+ </div>
177
+ </div>
178
+
179
+ {detail.definition && (
180
+ <PropSection label="Definition">
181
+ <p style={{ margin: 0, color: "#f0cdd3", fontSize: 13, lineHeight: 1.6 }}>
182
+ {detail.definition}
183
+ </p>
184
+ </PropSection>
185
+ )}
186
+
187
+ {detail.scope_note && (
188
+ <PropSection label="Scope Note">
189
+ <p style={{ margin: 0, color: "#c99aa3", fontSize: 12, lineHeight: 1.5 }}>
190
+ {detail.scope_note}
191
+ </p>
192
+ </PropSection>
193
+ )}
194
+
195
+ {detail.editorial_note && (
196
+ <PropSection label="Editorial Note">
197
+ <p style={{ margin: 0, color: "#c99aa3", fontSize: 12, lineHeight: 1.5 }}>
198
+ {detail.editorial_note}
199
+ </p>
200
+ </PropSection>
201
+ )}
202
+
203
+ {renderUriList("Broader", detail.broader)}
204
+ {renderUriList("Narrower", detail.narrower)}
205
+ {renderUriList("Related", detail.related)}
206
+ {renderUriList("Exact Match", detail.exact_match)}
207
+ {renderUriList("Close Match", detail.close_match)}
208
+ {renderUriList("Broad Match", detail.broad_match)}
209
+ {renderUriList("Narrow Match", detail.narrow_match)}
210
+
211
+ {detail.scheme_uri && (
212
+ <PropSection label="Concept Scheme">
213
+ <span style={{ color: "#f0cdd3", fontSize: 11, fontFamily: "monospace", wordBreak: "break-all" }}>
214
+ {detail.scheme_uri}
215
+ </span>
216
+ </PropSection>
217
+ )}
218
+ </div>
219
+ )}
220
+ </div>
221
+ );
222
+ }
223
+
224
+ function PropSection({ label, children }: { label: string; children: React.ReactNode }) {
225
+ return (
226
+ <div style={{ paddingTop: 10, borderTop: "1px solid rgba(255,255,255,0.05)", marginTop: 10 }}>
227
+ <div style={{ color: "#9c7a82", fontSize: 10, fontWeight: 700, textTransform: "uppercase" as const, letterSpacing: "0.07em", marginBottom: 5 }}>
228
+ {label}
229
+ </div>
230
+ {children}
231
+ </div>
232
+ );
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Concept tree node
237
+ // ---------------------------------------------------------------------------
238
+
239
+ function ConceptTreeNode({
240
+ concept,
241
+ depth,
242
+ selectedUri,
243
+ onSelect,
244
+ }: {
245
+ concept: ConceptNode;
246
+ depth: number;
247
+ selectedUri: string | null;
248
+ onSelect: (uri: string) => void;
249
+ }) {
250
+ const [expanded, setExpanded] = useState(depth === 0);
251
+ const children = concept.children ?? [];
252
+ const hasChildren = children.length > 0;
253
+ const isSelected = selectedUri === concept.uri;
254
+
255
+ return (
256
+ <>
257
+ <div
258
+ style={{
259
+ display: "flex",
260
+ alignItems: "center",
261
+ gap: 4,
262
+ paddingLeft: 10 + depth * 14,
263
+ paddingRight: 10,
264
+ paddingTop: 5,
265
+ paddingBottom: 5,
266
+ borderRadius: 7,
267
+ cursor: "pointer",
268
+ background: isSelected ? "rgba(255,45,75,0.12)" : "transparent",
269
+ transition: "120ms ease",
270
+ }}
271
+ onMouseEnter={(e) => {
272
+ if (!isSelected)
273
+ (e.currentTarget as HTMLDivElement).style.background = "rgba(255,45,75,0.06)";
274
+ }}
275
+ onMouseLeave={(e) => {
276
+ if (!isSelected)
277
+ (e.currentTarget as HTMLDivElement).style.background = "transparent";
278
+ }}
279
+ >
280
+ {hasChildren ? (
281
+ <button
282
+ onClick={(e) => { e.stopPropagation(); setExpanded((v) => !v); }}
283
+ style={expandBtnStyle}
284
+ >
285
+ {expanded ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
286
+ </button>
287
+ ) : (
288
+ <span style={{ width: 18, display: "inline-block", flexShrink: 0 }} />
289
+ )}
290
+
291
+ <span
292
+ onClick={() => onSelect(concept.uri)}
293
+ style={{
294
+ flex: 1,
295
+ overflow: "hidden",
296
+ textOverflow: "ellipsis",
297
+ whiteSpace: "nowrap",
298
+ color: isSelected ? "#fff0f2" : depth === 0 ? "#f0cdd3" : "#c99aa3",
299
+ fontSize: depth === 0 ? 13 : 12,
300
+ fontWeight: depth === 0 ? 600 : 400,
301
+ }}
302
+ >
303
+ {concept.pref_label || concept.uri}
304
+ </span>
305
+
306
+ {hasChildren && (
307
+ <span style={{ color: "#8f6d75", fontSize: 10, flexShrink: 0 }}>
308
+ {children.length}
309
+ </span>
310
+ )}
311
+ </div>
312
+
313
+ {expanded && hasChildren && children.map((child) => (
314
+ <ConceptTreeNode
315
+ key={child.uri}
316
+ concept={child}
317
+ depth={depth + 1}
318
+ selectedUri={selectedUri}
319
+ onSelect={onSelect}
320
+ />
321
+ ))}
322
+ </>
323
+ );
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // Scheme panel
328
+ // ---------------------------------------------------------------------------
329
+
330
+ function SchemePanel({
331
+ scheme,
332
+ selectedUri,
333
+ onSelectConcept,
334
+ searchQuery,
335
+ }: {
336
+ scheme: SKOSScheme;
337
+ selectedUri: string | null;
338
+ onSelectConcept: (uri: string) => void;
339
+ searchQuery: string;
340
+ }) {
341
+ const [expanded, setExpanded] = useState(true);
342
+ const [hierarchy, setHierarchy] = useState<ConceptNode[]>([]);
343
+ const [loading, setLoading] = useState(expanded);
344
+ const [error, setError] = useState("");
345
+
346
+ const [prevExpanded, setPrevExpanded] = useState(expanded);
347
+ const [prevSchemeUri, setPrevSchemeUri] = useState(scheme.uri);
348
+ if (expanded !== prevExpanded || scheme.uri !== prevSchemeUri) {
349
+ setPrevExpanded(expanded);
350
+ setPrevSchemeUri(scheme.uri);
351
+ if (expanded) {
352
+ setLoading(true);
353
+ setError("");
354
+ setHierarchy([]);
355
+ } else {
356
+ setLoading(false);
357
+ }
358
+ }
359
+
360
+ useEffect(() => {
361
+ let ignore = false;
362
+ if (!expanded) return;
363
+ fetch(`/api/vocabulary/hierarchy?scheme=${encodeURIComponent(scheme.uri)}`)
364
+ .then(async (r) => {
365
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
366
+ const data = await r.json();
367
+ if (r.status === 207 && !ignore) setError(data.message || "Warning: Partial success loading hierarchy.");
368
+ return data;
369
+ })
370
+ .then((data) => {
371
+ if (!ignore) setHierarchy(data);
372
+ })
373
+ .catch((err) => {
374
+ if (!ignore) {
375
+ setHierarchy([]);
376
+ setError(err instanceof Error ? err.message : "Failed to load hierarchy.");
377
+ }
378
+ })
379
+ .finally(() => {
380
+ if (!ignore) setLoading(false);
381
+ });
382
+ return () => {
383
+ ignore = true;
384
+ };
385
+ }, [scheme.uri, expanded]);
386
+
387
+ const totalConcepts = countConcepts(hierarchy);
388
+
389
+ const filterConcepts = (nodes: ConceptNode[], q: string): ConceptNode[] => {
390
+ if (!q) return nodes;
391
+ return nodes.flatMap((n) => {
392
+ const match = (n.pref_label + " " + (n.alt_labels?.join(" ") ?? "") + " " + (n.description ?? ""))
393
+ .toLowerCase()
394
+ .includes(q.toLowerCase());
395
+ const filteredChildren = filterConcepts(n.children ?? [], q);
396
+ if (match || filteredChildren.length > 0) {
397
+ return [{ ...n, children: filteredChildren }];
398
+ }
399
+ return [];
400
+ });
401
+ };
402
+
403
+ const displayedConcepts = filterConcepts(hierarchy, searchQuery);
404
+
405
+ return (
406
+ <div style={schemePanelStyle}>
407
+ <button onClick={() => setExpanded((v) => !v)} style={schemeHeaderBtnStyle}>
408
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
409
+ {expanded ? <ChevronDown size={13} color="#c99aa3" /> : <ChevronRight size={13} color="#c99aa3" />}
410
+ <span style={{ color: "#ffeaed", fontSize: 14, fontWeight: 700 }}>{scheme.title}</span>
411
+ </div>
412
+ <span style={{ color: "#9c7a82", fontSize: 11 }}>
413
+ {loading ? "…" : `${totalConcepts} concept${totalConcepts !== 1 ? "s" : ""}`}
414
+ </span>
415
+ </button>
416
+
417
+ {expanded && (
418
+ <div style={{ paddingBottom: 8 }}>
419
+ {error ? <div style={errorStyle}>{error}</div> : null}
420
+ {loading ? (
421
+ <div style={{ padding: "10px 20px", display: "flex", alignItems: "center", gap: 8 }}>
422
+ <Loader2 size={12} color="#ff2d4b" style={{ animation: "spin 0.8s linear infinite" }} />
423
+ <span style={{ color: "#9c7a82", fontSize: 12 }}>Loading concepts…</span>
424
+ </div>
425
+ ) : displayedConcepts.length === 0 ? (
426
+ <div style={{ padding: "8px 24px", color: "#9c7a82", fontSize: 12, fontStyle: "italic" }}>
427
+ {searchQuery ? "No matching concepts" : "No concepts in this scheme"}
428
+ </div>
429
+ ) : (
430
+ <div style={{ paddingTop: 2 }}>
431
+ {displayedConcepts.map((concept) => (
432
+ <ConceptTreeNode
433
+ key={concept.uri}
434
+ concept={concept}
435
+ depth={0}
436
+ selectedUri={selectedUri}
437
+ onSelect={onSelectConcept}
438
+ />
439
+ ))}
440
+ </div>
441
+ )}
442
+ </div>
443
+ )}
444
+ </div>
445
+ );
446
+ }
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Main SKOSVocabularyManager
450
+ // ---------------------------------------------------------------------------
451
+
452
+ interface Props {
453
+ schemeUri?: string;
454
+ }
455
+
456
+ export function SKOSVocabularyManager({ schemeUri }: Props) {
457
+ const [schemes, setSchemes] = useState<SKOSScheme[]>([]);
458
+ const [loading, setLoading] = useState(true);
459
+ const [error, setError] = useState("");
460
+ const [searchQ, setSearchQ] = useState("");
461
+ const [selectedUri, setSelectedUri] = useState<string | null>(null);
462
+
463
+ useEffect(() => {
464
+ fetch("/api/ontology/skos/schemes")
465
+ .then(async (r) => {
466
+ if (!r.ok) throw new Error(`Failed to load schemes (${r.status})`);
467
+ const data = await r.json();
468
+ if (r.status === 207) setError(data.message || "Warning: Partial success loading schemes.");
469
+ return data;
470
+ })
471
+ .then(setSchemes)
472
+ .catch((e) => setError(e.message))
473
+ .finally(() => setLoading(false));
474
+ }, []);
475
+
476
+ const displayedSchemes = schemeUri
477
+ ? schemes.filter((s) => s.uri === schemeUri)
478
+ : schemes;
479
+
480
+ return (
481
+ <div style={managerShellStyle}>
482
+ {error ? <div style={errorStyle}>{error}</div> : null}
483
+ {/* Search bar */}
484
+ <div style={skosToolbarStyle}>
485
+ <div style={skosSearchBarStyle}>
486
+ <Search size={13} color="#9c7a82" />
487
+ <input
488
+ value={searchQ}
489
+ onChange={(e) => setSearchQ(e.target.value)}
490
+ placeholder="Search labels and definitions…"
491
+ style={skosSearchInputStyle}
492
+ />
493
+ {searchQ && (
494
+ <button onClick={() => setSearchQ("")} style={iconBtnStyle}>
495
+ <X size={11} />
496
+ </button>
497
+ )}
498
+ </div>
499
+ </div>
500
+
501
+ <div style={skosBodyStyle}>
502
+ {/* Scheme tree column */}
503
+ <div style={treeColStyle}>
504
+ {loading && (
505
+ <div style={centerStyle}>
506
+ <Loader2 size={18} color="#ff2d4b" style={{ animation: "spin 1s linear infinite" }} />
507
+ </div>
508
+ )}
509
+
510
+ {error && (
511
+ <div style={centerStyle}>
512
+ <AlertCircle size={16} color="#ff9daf" />
513
+ <span style={{ color: "#ff9daf", fontSize: 12, marginTop: 6 }}>{error}</span>
514
+ </div>
515
+ )}
516
+
517
+ {!loading && !error && displayedSchemes.length === 0 && (
518
+ <div style={{ ...centerStyle, textAlign: "center", padding: 28 }}>
519
+ <BookOpen size={28} color="rgba(158,232,215,0.15)" />
520
+ <span style={{ color: "#c99aa3", fontSize: 12, marginTop: 10 }}>
521
+ No SKOS concept schemes found
522
+ </span>
523
+ <span style={{ color: "#9c7a82", fontSize: 11, marginTop: 4, maxWidth: 220 }}>
524
+ Import a SKOS vocabulary to browse concepts here
525
+ </span>
526
+ </div>
527
+ )}
528
+
529
+ {!loading && displayedSchemes.map((scheme) => (
530
+ <SchemePanel
531
+ key={scheme.uri}
532
+ scheme={scheme}
533
+ selectedUri={selectedUri}
534
+ onSelectConcept={setSelectedUri}
535
+ searchQuery={searchQ}
536
+ />
537
+ ))}
538
+ </div>
539
+
540
+ {/* Concept detail panel */}
541
+ {selectedUri && (
542
+ <ConceptDetailPanel
543
+ uri={selectedUri}
544
+ onClose={() => setSelectedUri(null)}
545
+ onNavigate={setSelectedUri}
546
+ />
547
+ )}
548
+ </div>
549
+ </div>
550
+ );
551
+ }
552
+
553
+ /* ─── styles ─────────────────────────────────────────────────────────── */
554
+
555
+ const managerShellStyle: React.CSSProperties = {
556
+ display: "flex",
557
+ flexDirection: "column",
558
+ height: "100%",
559
+ overflow: "hidden",
560
+ };
561
+
562
+ const skosToolbarStyle: React.CSSProperties = {
563
+ padding: "10px 12px",
564
+ borderBottom: "1px solid rgba(255,107,129,0.08)",
565
+ flexShrink: 0,
566
+ };
567
+
568
+ const skosSearchBarStyle: React.CSSProperties = {
569
+ display: "flex",
570
+ alignItems: "center",
571
+ gap: 7,
572
+ padding: "6px 10px",
573
+ borderRadius: 8,
574
+ border: "1px solid rgba(255,107,129,0.12)",
575
+ background: "rgba(0,0,0,0.22)",
576
+ };
577
+
578
+ const skosSearchInputStyle: React.CSSProperties = {
579
+ flex: 1,
580
+ background: "transparent",
581
+ border: "none",
582
+ outline: "none",
583
+ color: "#fff0f2",
584
+ fontSize: 12,
585
+ };
586
+
587
+ const skosBodyStyle: React.CSSProperties = {
588
+ flex: 1,
589
+ minHeight: 0,
590
+ display: "flex",
591
+ overflow: "hidden",
592
+ };
593
+
594
+ const treeColStyle: React.CSSProperties = {
595
+ flex: 1,
596
+ overflowY: "auto",
597
+ padding: "8px 6px",
598
+ };
599
+
600
+ const detailPanelStyle: React.CSSProperties = {
601
+ width: 300,
602
+ flexShrink: 0,
603
+ display: "flex",
604
+ flexDirection: "column",
605
+ borderLeft: "1px solid rgba(255,107,129,0.1)",
606
+ background: "rgba(6,3,5,0.5)",
607
+ overflow: "hidden",
608
+ };
609
+
610
+ const detailHeaderStyle: React.CSSProperties = {
611
+ display: "flex",
612
+ alignItems: "center",
613
+ justifyContent: "space-between",
614
+ padding: "12px 14px",
615
+ borderBottom: "1px solid rgba(255,107,129,0.08)",
616
+ flexShrink: 0,
617
+ };
618
+
619
+ const detailBodyStyle: React.CSSProperties = {
620
+ flex: 1,
621
+ overflowY: "auto",
622
+ padding: "14px",
623
+ };
624
+
625
+ const schemePanelStyle: React.CSSProperties = {
626
+ borderRadius: 10,
627
+ border: "1px solid rgba(255,107,129,0.1)",
628
+ background: "rgba(255,255,255,0.02)",
629
+ overflow: "hidden",
630
+ marginBottom: 8,
631
+ };
632
+
633
+ const schemeHeaderBtnStyle: React.CSSProperties = {
634
+ width: "100%",
635
+ display: "flex",
636
+ alignItems: "center",
637
+ justifyContent: "space-between",
638
+ padding: "10px 12px",
639
+ background: "transparent",
640
+ border: "none",
641
+ cursor: "pointer",
642
+ borderBottom: "1px solid rgba(255,255,255,0.05)",
643
+ };
644
+
645
+ const expandBtnStyle: React.CSSProperties = {
646
+ background: "transparent",
647
+ border: "none",
648
+ color: "#c99aa3",
649
+ cursor: "pointer",
650
+ padding: 0,
651
+ display: "flex",
652
+ alignItems: "center",
653
+ flexShrink: 0,
654
+ width: 18,
655
+ };
656
+
657
+ const chipStyle: React.CSSProperties = {
658
+ padding: "2px 8px",
659
+ borderRadius: 999,
660
+ background: "rgba(255,255,255,0.05)",
661
+ border: "1px solid rgba(255,255,255,0.08)",
662
+ color: "#c99aa3",
663
+ fontSize: 11,
664
+ };
665
+
666
+ const iconBtnStyle: React.CSSProperties = {
667
+ background: "transparent",
668
+ border: "none",
669
+ color: "#c99aa3",
670
+ cursor: "pointer",
671
+ padding: 2,
672
+ display: "grid",
673
+ placeItems: "center",
674
+ };
675
+
676
+ const navLinkStyle: React.CSSProperties = {
677
+ display: "inline-flex",
678
+ alignItems: "center",
679
+ gap: 5,
680
+ background: "transparent",
681
+ border: "none",
682
+ cursor: "pointer",
683
+ padding: "2px 0",
684
+ textAlign: "left",
685
+ };
686
+
687
+ const errorStyle: React.CSSProperties = { padding: 12, borderRadius: 14, color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)", margin: "0 10px 10px 10px", fontSize: 12 };
688
+
689
+ const centerStyle: React.CSSProperties = {
690
+ display: "flex",
691
+ flexDirection: "column",
692
+ alignItems: "center",
693
+ justifyContent: "center",
694
+ height: "100%",
695
+ padding: 24,
696
+ };