@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,770 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import {
3
+ ReactFlow,
4
+ Background,
5
+ Controls,
6
+ MiniMap,
7
+ addEdge,
8
+ useNodesState,
9
+ useEdgesState,
10
+ MarkerType,
11
+ Handle,
12
+ Position,
13
+ } from "@xyflow/react";
14
+ import type { Connection, Edge, Node, ReactFlowInstance } from "@xyflow/react";
15
+ import "@xyflow/react/dist/style.css";
16
+ import {
17
+ Plus,
18
+ GitBranch,
19
+ User,
20
+ Shield,
21
+ FileText,
22
+ Layout,
23
+ Send,
24
+ Pencil,
25
+ Trash2,
26
+ } from "lucide-react";
27
+ import { loadOntologyEntityOwner, loadOntologyGraph } from "./api";
28
+ import type { OntologyGraphEdge, OntologyGraphNode } from "./api";
29
+ import {
30
+ isEditableEntityType,
31
+ ONTOLOGY_MINIMAP_THEME,
32
+ resolveEditorOntology,
33
+ } from "./ontologyEditorModel";
34
+ import type { EditorEntityType, RegistryEntry } from "./ontologyEditorModel";
35
+ import { clearEntitySelection, readOntologyUrlState, writeEntitySelection } from "./ontologyUrlState";
36
+
37
+ type OntologyNodeData = {
38
+ label?: string;
39
+ type?: string;
40
+ entityType?: EditorEntityType;
41
+ };
42
+
43
+ type OntologyNode = Node<OntologyNodeData>;
44
+ type OntologyEdge = Edge<Record<string, unknown>>;
45
+
46
+ const nodeTypes = {
47
+ classNode: ({ data }: { data: OntologyNodeData }) => (
48
+ <div style={classNodeStyle}>
49
+ <Handle type="target" position={Position.Left} style={handleStyle} />
50
+ <div style={classNodeHeader}>{data.label}</div>
51
+ <div style={classNodeSub}>{data.type}</div>
52
+ <Handle type="source" position={Position.Right} style={handleStyle} />
53
+ </div>
54
+ ),
55
+ };
56
+
57
+ const handleStyle: React.CSSProperties = {
58
+ width: 8,
59
+ height: 8,
60
+ border: "1px solid rgba(235, 243, 255, 0.8)",
61
+ background: "#ff2d4b",
62
+ };
63
+
64
+ const ontologyFlowThemeCss = `
65
+ .ontology-editor-flow .react-flow__controls {
66
+ overflow: hidden;
67
+ border: 1px solid rgba(255, 107, 129, 0.2);
68
+ border-radius: 9px;
69
+ background: rgba(6, 13, 26, 0.96);
70
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.38);
71
+ }
72
+
73
+ .ontology-editor-flow .react-flow__controls-button {
74
+ width: 30px;
75
+ height: 30px;
76
+ background: transparent;
77
+ border-bottom-color: rgba(255, 107, 129, 0.14);
78
+ color: #c99aa3;
79
+ transition: color 140ms ease, background 140ms ease;
80
+ }
81
+
82
+ .ontology-editor-flow .react-flow__controls-button:hover {
83
+ background: rgba(255, 45, 75, 0.14);
84
+ color: #fff0f2;
85
+ }
86
+
87
+ .ontology-editor-flow .react-flow__controls-button:focus-visible {
88
+ position: relative;
89
+ z-index: 1;
90
+ outline: 2px solid #ff6b81;
91
+ outline-offset: -2px;
92
+ }
93
+
94
+ .ontology-editor-flow .react-flow__controls-button:disabled {
95
+ background: rgba(6, 3, 5, 0.32);
96
+ color: #40566f;
97
+ }
98
+ `;
99
+
100
+ const classNodeStyle: React.CSSProperties = {
101
+ padding: "12px 16px",
102
+ borderRadius: "8px",
103
+ background: "linear-gradient(135deg, rgba(255, 45, 75, 0.15), rgba(255, 45, 75, 0.05))",
104
+ border: "1px solid rgba(255, 107, 129, 0.3)",
105
+ color: "#fff0f2",
106
+ fontSize: "13px",
107
+ fontWeight: "600",
108
+ minWidth: "140px",
109
+ textAlign: "center",
110
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.2)",
111
+ };
112
+
113
+ const classNodeHeader: React.CSSProperties = {
114
+ fontSize: "14px",
115
+ fontWeight: "700",
116
+ marginBottom: "4px",
117
+ };
118
+
119
+ const classNodeSub: React.CSSProperties = {
120
+ fontSize: "11px",
121
+ color: "#c99aa3",
122
+ fontWeight: "500",
123
+ };
124
+
125
+ interface DraftDiff {
126
+ added_classes: string[];
127
+ removed_classes: string[];
128
+ modified_classes: Record<string, Record<string, any>>;
129
+ added_properties: string[];
130
+ removed_properties: string[];
131
+ modified_properties: Record<string, Record<string, any>>;
132
+ added_restrictions: Record<string, any>[];
133
+ removed_restrictions: Record<string, any>[];
134
+ added_axioms: Record<string, any>[];
135
+ removed_axioms: Record<string, any>[];
136
+ annotation_changes: Record<string, Record<string, any>>;
137
+ }
138
+
139
+ function requestedEntityUri(): string {
140
+ return readOntologyUrlState().entityUri || "";
141
+ }
142
+
143
+ function nodeLabel(node: OntologyGraphNode): string {
144
+ const explicit = String(node.content || node.properties?.["rdfs:label"] || "").trim();
145
+ if (explicit && explicit !== node.id) {
146
+ return explicit;
147
+ }
148
+ const trimmed = node.id.replace(/[/#]+$/, "");
149
+ return trimmed.split("#").pop() || trimmed.split("/").pop() || node.id;
150
+ }
151
+
152
+ // The backend's remaining kinds (concept, scheme, individual, unknown) have no
153
+ // editor affordances, so they collapse into the read-only "external" kind. That
154
+ // reading belongs here rather than on the wire: "external reference material"
155
+ // is what the editor does about an unnameable type, not what the type is.
156
+ function classifyEditorNode(node: OntologyGraphNode): OntologyNodeData["entityType"] {
157
+ switch (node.entity_type) {
158
+ case "ontology":
159
+ case "class":
160
+ case "property":
161
+ return node.entity_type;
162
+ default:
163
+ return "external";
164
+ }
165
+ }
166
+
167
+ function layoutEditorNodes(inputNodes: OntologyNode[]): OntologyNode[] {
168
+ const properties = inputNodes.filter((node) => node.data.entityType === "property");
169
+ const targets = inputNodes.filter((node) => (
170
+ node.data.entityType === "class" || node.data.entityType === "external"
171
+ ));
172
+ const context = inputNodes.filter((node) => (
173
+ node.data.entityType !== "property"
174
+ && node.data.entityType !== "class"
175
+ && node.data.entityType !== "external"
176
+ ));
177
+ const height = Math.max(360, Math.max(properties.length, targets.length) * 180);
178
+ const positions = new Map<string, { x: number; y: number }>();
179
+
180
+ properties.forEach((node, index) => {
181
+ positions.set(node.id, { x: 0, y: ((index + 1) * height) / (properties.length + 1) });
182
+ });
183
+ targets.forEach((node, index) => {
184
+ positions.set(node.id, { x: 600, y: ((index + 1) * height) / (targets.length + 1) });
185
+ });
186
+ context.forEach((node, index) => {
187
+ positions.set(node.id, { x: 300 + index * 220, y: height + 120 });
188
+ });
189
+
190
+ return inputNodes.map((node) => ({
191
+ ...node,
192
+ position: positions.get(node.id) || node.position,
193
+ }));
194
+ }
195
+
196
+ function buildEditorElements(apiNodes: OntologyGraphNode[], apiEdges: OntologyGraphEdge[]) {
197
+ const sortedNodes = [...apiNodes].sort((left, right) => {
198
+ const typeDelta = left.type.localeCompare(right.type);
199
+ return typeDelta || left.id.localeCompare(right.id);
200
+ });
201
+ const nodes = layoutEditorNodes(sortedNodes.map((node) => ({
202
+ id: node.id,
203
+ type: "classNode",
204
+ position: { x: 0, y: 0 },
205
+ data: {
206
+ label: nodeLabel(node),
207
+ type: node.type,
208
+ entityType: classifyEditorNode(node),
209
+ },
210
+ })));
211
+ const edges: OntologyEdge[] = apiEdges.map((edge, index) => ({
212
+ id: edge.id || `${edge.source}:${edge.type}:${edge.target}:${index}`,
213
+ source: edge.source,
214
+ target: edge.target,
215
+ label: edge.type,
216
+ type: "default",
217
+ markerEnd: { type: MarkerType.ArrowClosed },
218
+ style: { stroke: "rgba(255, 107, 129, 0.72)", strokeWidth: 1.5 },
219
+ labelStyle: { fill: "#c8dcf5", fontSize: 11, fontWeight: 600 },
220
+ labelBgStyle: { fill: "#0b0508", fillOpacity: 0.9 },
221
+ }));
222
+ return { nodes, edges };
223
+ }
224
+
225
+ export function OntologyEditor() {
226
+ const [nodes, setNodes, onNodesChange] = useNodesState<OntologyNode>([]);
227
+ const [edges, setEdges, onEdgesChange] = useEdgesState<OntologyEdge>([]);
228
+ const [selectedElement, setSelectedElement] = useState<OntologyNode | OntologyEdge | null>(null);
229
+ const hasDetailPanel = selectedElement !== null;
230
+ const [registry, setRegistry] = useState<RegistryEntry[]>([]);
231
+ const [ontologyUri, setOntologyUri] = useState<string>("");
232
+ const [flowInstance, setFlowInstance] = useState<ReactFlowInstance<OntologyNode, OntologyEdge> | null>(null);
233
+ const [isLoadingGraph, setIsLoadingGraph] = useState(false);
234
+ const [graphError, setGraphError] = useState("");
235
+ const [unownedEntity, setUnownedEntity] = useState("");
236
+ const [draftDiff, setDraftDiff] = useState<DraftDiff>({
237
+ added_classes: [],
238
+ removed_classes: [],
239
+ modified_classes: {},
240
+ added_properties: [],
241
+ removed_properties: [],
242
+ modified_properties: {},
243
+ added_restrictions: [],
244
+ removed_restrictions: [],
245
+ added_axioms: [],
246
+ removed_axioms: [],
247
+ annotation_changes: {},
248
+ });
249
+ const [isSaving, setIsSaving] = useState(false);
250
+ const [showContext, setShowContext] = useState<{ x: number; y: number; type: string; element: OntologyNode | OntologyEdge } | null>(null);
251
+
252
+ useEffect(() => {
253
+ let cancelled = false;
254
+ const requested = requestedEntityUri();
255
+ Promise.all([
256
+ fetch("/api/ontology/registry").then((response) => (response.ok ? response.json() : [])),
257
+ requested
258
+ ? loadOntologyEntityOwner(requested).catch(() => undefined)
259
+ : Promise.resolve(undefined),
260
+ ])
261
+ .then(([entries, ownerVerdict]: [RegistryEntry[], string | null | undefined]) => {
262
+ if (cancelled) return;
263
+ setRegistry(entries);
264
+ const resolution = resolveEditorOntology(entries, requested, ownerVerdict);
265
+ // The registry default is the right landing place for "no entity asked
266
+ // for", but not for "the backend says nothing owns the entity that was
267
+ // asked for" — that would open an arbitrary ontology whose graph
268
+ // excludes the entity, and report nothing about why.
269
+ if (resolution.status === "unowned") {
270
+ setUnownedEntity(resolution.entityUri);
271
+ return;
272
+ }
273
+ setUnownedEntity("");
274
+ const resolvedOntology = resolution.status === "resolved" ? resolution.uri : undefined;
275
+ setOntologyUri((current) => current || resolvedOntology || entries[0]?.uri || "");
276
+ })
277
+ .catch((error) => {
278
+ console.error("Failed to load ontology registry:", error);
279
+ });
280
+ return () => {
281
+ cancelled = true;
282
+ };
283
+ }, []);
284
+
285
+ useEffect(() => {
286
+ if (!ontologyUri) {
287
+ setNodes([]);
288
+ setEdges([]);
289
+ setSelectedElement(null);
290
+ return;
291
+ }
292
+
293
+ const controller = new AbortController();
294
+ setIsLoadingGraph(true);
295
+ setGraphError("");
296
+ loadOntologyGraph(ontologyUri, controller.signal)
297
+ .then((payload) => {
298
+ const elements = buildEditorElements(payload.nodes, payload.edges);
299
+ setNodes(elements.nodes);
300
+ setEdges(elements.edges);
301
+ const requested = requestedEntityUri();
302
+ setSelectedElement(elements.nodes.find((node) => node.id === requested) || null);
303
+ })
304
+ .catch((error) => {
305
+ if (controller.signal.aborted) return;
306
+ setNodes([]);
307
+ setEdges([]);
308
+ setSelectedElement(null);
309
+ setGraphError(error instanceof Error ? error.message : "Failed to load ontology graph");
310
+ })
311
+ .finally(() => {
312
+ if (!controller.signal.aborted) setIsLoadingGraph(false);
313
+ });
314
+
315
+ return () => controller.abort();
316
+ }, [ontologyUri, setEdges, setNodes]);
317
+
318
+ useEffect(() => {
319
+ if (!flowInstance || nodes.length === 0) return;
320
+ const frame = window.requestAnimationFrame(() => {
321
+ void flowInstance.fitView({ padding: 0.22, duration: 320, maxZoom: 1.25 });
322
+ });
323
+ return () => window.cancelAnimationFrame(frame);
324
+ }, [flowInstance, hasDetailPanel, nodes.length, ontologyUri]);
325
+
326
+ const onConnect = useCallback(
327
+ (params: Connection) => setEdges((eds) => addEdge({ ...params, markerEnd: { type: MarkerType.ArrowClosed } }, eds)),
328
+ [setEdges]
329
+ );
330
+
331
+ const addClass = useCallback(() => {
332
+ const newId = `class_${Date.now()}`;
333
+ const newNode: OntologyNode = {
334
+ id: newId,
335
+ type: "classNode",
336
+ position: { x: Math.random() * 400, y: Math.random() * 300 },
337
+ data: { label: "NewClass", type: "owl:Class", entityType: "class" },
338
+ };
339
+ setNodes((nds) => [...nds, newNode]);
340
+ setDraftDiff((prev) => ({
341
+ ...prev,
342
+ added_classes: [...prev.added_classes, newId],
343
+ }));
344
+ }, [setNodes]);
345
+
346
+ const addProperty = useCallback(() => {
347
+ if (nodes.length < 2) {
348
+ alert("Add at least two classes before creating a property edge.");
349
+ return;
350
+ }
351
+ const newId = `prop_${Date.now()}`;
352
+ const newEdge: OntologyEdge = {
353
+ id: newId,
354
+ source: nodes[0].id,
355
+ target: nodes[1].id,
356
+ label: "hasProperty",
357
+ type: "smoothstep",
358
+ animated: true,
359
+ };
360
+ setEdges((eds) => [...eds, newEdge]);
361
+ setDraftDiff((prev) => ({
362
+ ...prev,
363
+ added_properties: [...prev.added_properties, newId],
364
+ }));
365
+ }, [nodes, setEdges]);
366
+
367
+ const addIndividual = useCallback(() => {
368
+ const newId = `ind_${Date.now()}`;
369
+ const newNode: OntologyNode = {
370
+ id: newId,
371
+ type: "classNode",
372
+ position: { x: Math.random() * 400, y: Math.random() * 300 },
373
+ data: { label: "NewIndividual", type: "owl:NamedIndividual", entityType: "external" },
374
+ };
375
+ setNodes((nds) => [...nds, newNode]);
376
+ }, [setNodes]);
377
+
378
+ const addRestriction = useCallback(() => {
379
+ setDraftDiff((prev) => ({
380
+ ...prev,
381
+ added_restrictions: [...prev.added_restrictions, { type: "someValuesFrom", value: "" }],
382
+ }));
383
+ }, []);
384
+
385
+ const addAxiom = useCallback(() => {
386
+ setDraftDiff((prev) => ({
387
+ ...prev,
388
+ added_axioms: [...prev.added_axioms, { type: "subClassOf", value: "" }],
389
+ }));
390
+ }, []);
391
+
392
+ const autoLayout = useCallback(() => {
393
+ setNodes(layoutEditorNodes(nodes));
394
+ }, [nodes, setNodes]);
395
+
396
+ const selectNode = useCallback((node: OntologyNode) => {
397
+ setSelectedElement(node);
398
+ writeEntitySelection(node.id);
399
+ }, []);
400
+
401
+ const saveDraft = useCallback(async () => {
402
+ if (!ontologyUri) {
403
+ alert("Please select an ontology first");
404
+ return;
405
+ }
406
+ setIsSaving(true);
407
+ try {
408
+ const response = await fetch("/api/ontology/draft", {
409
+ method: "PATCH",
410
+ headers: { "Content-Type": "application/json" },
411
+ body: JSON.stringify({
412
+ ontology_uri: ontologyUri,
413
+ diff: draftDiff,
414
+ author: "user",
415
+ summary: "Visual editor changes",
416
+ }),
417
+ });
418
+ if (response.ok) {
419
+ const data = await response.json();
420
+ alert(`Draft saved: ${data.draft_id}`);
421
+ }
422
+ } catch (error) {
423
+ console.error("Failed to save draft:", error);
424
+ alert("Failed to save draft");
425
+ } finally {
426
+ setIsSaving(false);
427
+ }
428
+ }, [ontologyUri, draftDiff]);
429
+
430
+ const handleNodeContextMenu = useCallback((event: React.MouseEvent, node: OntologyNode) => {
431
+ event.preventDefault();
432
+ setSelectedElement(node);
433
+ setShowContext({ x: event.clientX, y: event.clientY, type: "node", element: node });
434
+ }, []);
435
+
436
+ const handleEdgeContextMenu = useCallback((event: React.MouseEvent, edge: OntologyEdge) => {
437
+ event.preventDefault();
438
+ setSelectedElement(edge);
439
+ setShowContext({ x: event.clientX, y: event.clientY, type: "edge", element: edge });
440
+ }, []);
441
+
442
+ const deleteSelected = useCallback(() => {
443
+ const target = showContext?.element ?? selectedElement;
444
+ if (target) {
445
+ if ("source" in target) {
446
+ setEdges((eds) => eds.filter((e) => e.id !== target.id));
447
+ setDraftDiff((prev) => ({
448
+ ...prev,
449
+ removed_properties: [...prev.removed_properties, target.id],
450
+ }));
451
+ } else if (isEditableEntityType(target.data.entityType)) {
452
+ setNodes((nds) => nds.filter((n) => n.id !== target.id));
453
+ setDraftDiff((prev) => target.data.entityType === "property"
454
+ ? { ...prev, removed_properties: [...prev.removed_properties, target.id] }
455
+ : { ...prev, removed_classes: [...prev.removed_classes, target.id] });
456
+ }
457
+ setSelectedElement(null);
458
+ }
459
+ setShowContext(null);
460
+ }, [selectedElement, setNodes, setEdges, showContext]);
461
+
462
+ const renameSelected = useCallback(() => {
463
+ const target = showContext?.element ?? selectedElement;
464
+ if (target && !("source" in target) && isEditableEntityType(target.data.entityType)) {
465
+ const newLabel = prompt("Enter new name:", String(target.data.label ?? ""));
466
+ if (newLabel) {
467
+ setNodes((nds) =>
468
+ nds.map((n) => (n.id === target.id ? { ...n, data: { ...n.data, label: newLabel } } : n))
469
+ );
470
+ setDraftDiff((prev) => target.data.entityType === "property"
471
+ ? {
472
+ ...prev,
473
+ modified_properties: { ...prev.modified_properties, [target.id]: { label: newLabel } },
474
+ }
475
+ : {
476
+ ...prev,
477
+ modified_classes: { ...prev.modified_classes, [target.id]: { label: newLabel } },
478
+ });
479
+ }
480
+ }
481
+ setShowContext(null);
482
+ }, [selectedElement, setNodes, showContext]);
483
+
484
+ useEffect(() => {
485
+ const handleClick = () => setShowContext(null);
486
+ window.addEventListener("click", handleClick);
487
+ return () => window.removeEventListener("click", handleClick);
488
+ }, []);
489
+
490
+ const toolbarStyle: React.CSSProperties = {
491
+ display: "flex",
492
+ gap: "8px",
493
+ padding: "12px 16px",
494
+ background: "rgba(6, 3, 5, 0.92)",
495
+ borderBottom: "1px solid rgba(255, 120, 140, 0.12)",
496
+ flexWrap: "wrap",
497
+ };
498
+
499
+ const toolbarButtonStyle: React.CSSProperties = {
500
+ display: "inline-flex",
501
+ alignItems: "center",
502
+ gap: "6px",
503
+ padding: "8px 12px",
504
+ borderRadius: "8px",
505
+ border: "1px solid rgba(255, 107, 129, 0.18)",
506
+ background: "rgba(255, 45, 75, 0.08)",
507
+ color: "#fff0f2",
508
+ fontSize: "12px",
509
+ fontWeight: "600",
510
+ cursor: "pointer",
511
+ transition: "160ms ease",
512
+ };
513
+
514
+ const selectStyle: React.CSSProperties = {
515
+ padding: "8px 12px",
516
+ borderRadius: "8px",
517
+ border: "1px solid rgba(255, 107, 129, 0.18)",
518
+ background: "rgba(6, 3, 5, 0.88)",
519
+ color: "#fff0f2",
520
+ fontSize: "12px",
521
+ minWidth: "260px",
522
+ };
523
+
524
+ const contextMenuStyle: React.CSSProperties = {
525
+ position: "fixed",
526
+ background: "rgba(12, 6, 9, 0.95)",
527
+ border: "1px solid rgba(255, 107, 129, 0.3)",
528
+ borderRadius: "8px",
529
+ padding: "8px 0",
530
+ minWidth: "180px",
531
+ boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
532
+ zIndex: 1000,
533
+ };
534
+
535
+ const contextItemStyle: React.CSSProperties = {
536
+ padding: "8px 16px",
537
+ display: "flex",
538
+ alignItems: "center",
539
+ gap: "10px",
540
+ color: "#fff0f2",
541
+ fontSize: "13px",
542
+ cursor: "pointer",
543
+ transition: "160ms ease",
544
+ };
545
+
546
+ const detailPanelStyle: React.CSSProperties = {
547
+ flex: "0 0 320px",
548
+ width: "320px",
549
+ minWidth: "320px",
550
+ boxSizing: "border-box",
551
+ background: "rgba(12, 6, 9, 0.95)",
552
+ borderLeft: "1px solid rgba(255, 120, 140, 0.12)",
553
+ padding: "20px",
554
+ overflow: "auto",
555
+ backdropFilter: "blur(18px)",
556
+ };
557
+
558
+ return (
559
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", background: "#0b0508" }}>
560
+ <style>{ontologyFlowThemeCss}</style>
561
+ <div style={toolbarStyle}>
562
+ <select
563
+ aria-label="Active ontology"
564
+ value={ontologyUri}
565
+ onChange={(event) => {
566
+ setOntologyUri(event.target.value);
567
+ setSelectedElement(null);
568
+ setUnownedEntity("");
569
+ clearEntitySelection();
570
+ }}
571
+ style={selectStyle}
572
+ >
573
+ <option value="">Select ontology...</option>
574
+ {registry.map((entry) => (
575
+ <option key={entry.uri} value={entry.uri}>
576
+ {entry.name || entry.uri}
577
+ </option>
578
+ ))}
579
+ </select>
580
+ <button style={toolbarButtonStyle} onClick={addClass}>
581
+ <Plus size={14} />
582
+ Add Class
583
+ </button>
584
+ <button style={toolbarButtonStyle} onClick={addProperty} disabled={nodes.length < 2}>
585
+ <GitBranch size={14} />
586
+ Add Property
587
+ </button>
588
+ <button style={toolbarButtonStyle} onClick={addIndividual}>
589
+ <User size={14} />
590
+ Add Individual
591
+ </button>
592
+ <button style={toolbarButtonStyle} onClick={addRestriction}>
593
+ <Shield size={14} />
594
+ Add Restriction
595
+ </button>
596
+ <button style={toolbarButtonStyle} onClick={addAxiom}>
597
+ <FileText size={14} />
598
+ Add Axiom
599
+ </button>
600
+ <button style={toolbarButtonStyle} onClick={autoLayout}>
601
+ <Layout size={14} />
602
+ Auto Layout
603
+ </button>
604
+ <div style={{ flex: 1 }} />
605
+ <button style={toolbarButtonStyle} onClick={saveDraft} disabled={isSaving}>
606
+ <Send size={14} />
607
+ {isSaving ? "Saving..." : "Propose"}
608
+ </button>
609
+ </div>
610
+
611
+ <div style={{ display: "flex", flex: 1, minHeight: 0, minWidth: 0 }}>
612
+ <div style={{ flex: 1, minHeight: 0, minWidth: 0, position: "relative" }}>
613
+ <ReactFlow
614
+ className="ontology-editor-flow"
615
+ nodes={nodes}
616
+ edges={edges}
617
+ onNodesChange={onNodesChange}
618
+ onEdgesChange={onEdgesChange}
619
+ onConnect={onConnect}
620
+ onInit={setFlowInstance}
621
+ onNodeClick={(_, node) => selectNode(node)}
622
+ onEdgeClick={(_, edge) => setSelectedElement(edge)}
623
+ onNodeContextMenu={handleNodeContextMenu}
624
+ onEdgeContextMenu={handleEdgeContextMenu}
625
+ nodeTypes={nodeTypes}
626
+ fitView
627
+ style={{ background: "#0b0508" }}
628
+ >
629
+ <Background color="#1a2d3d" gap={20} />
630
+ <Controls />
631
+ <MiniMap {...ONTOLOGY_MINIMAP_THEME} />
632
+ </ReactFlow>
633
+
634
+ {isLoadingGraph && (
635
+ <div style={canvasMessageStyle}>Loading ontology structure…</div>
636
+ )}
637
+ {!isLoadingGraph && graphError && (
638
+ <div style={{ ...canvasMessageStyle, color: "#ff9a8d" }}>{graphError}</div>
639
+ )}
640
+ {!isLoadingGraph && !graphError && unownedEntity && (
641
+ <div style={{ ...canvasMessageStyle, color: "#ff7a45" }}>
642
+ No registered ontology owns {unownedEntity}. Pick an ontology above to start editing.
643
+ </div>
644
+ )}
645
+ {!isLoadingGraph && !graphError && !unownedEntity && ontologyUri && nodes.length === 0 && (
646
+ <div style={canvasMessageStyle}>This ontology has no editable classes or properties.</div>
647
+ )}
648
+
649
+ {showContext && (
650
+ <div style={{ ...contextMenuStyle, left: showContext.x, top: showContext.y }}>
651
+ {"source" in showContext.element || isEditableEntityType(showContext.element.data.entityType) ? (
652
+ <>
653
+ {!("source" in showContext.element) && (
654
+ <div style={contextItemStyle} onClick={renameSelected}>
655
+ <Pencil size={14} />
656
+ Rename
657
+ </div>
658
+ )}
659
+ <div style={contextItemStyle} onClick={deleteSelected}>
660
+ <Trash2 size={14} />
661
+ Delete
662
+ </div>
663
+ </>
664
+ ) : (
665
+ <div style={{ ...contextItemStyle, cursor: "default", color: "#c99aa3" }}>
666
+ This term is read-only
667
+ </div>
668
+ )}
669
+ </div>
670
+ )}
671
+ </div>
672
+
673
+ {selectedElement && (
674
+ <div style={detailPanelStyle}>
675
+ <h3 style={{ margin: "0 0 16px", color: "#fff0f2", fontSize: "16px" }}>
676
+ {"source" in selectedElement
677
+ ? "Relationship Details"
678
+ : selectedElement.data.entityType === "property"
679
+ ? "Property Details"
680
+ : selectedElement.data.entityType === "ontology"
681
+ ? "Ontology Details"
682
+ : selectedElement.data.entityType === "external"
683
+ ? "External Term Details"
684
+ : "Class Details"}
685
+ </h3>
686
+ <div style={{ marginBottom: "12px" }}>
687
+ <label style={{ display: "block", color: "#c99aa3", fontSize: "12px", marginBottom: "4px" }}>
688
+ ID
689
+ </label>
690
+ <div style={{ color: "#fff0f2", fontSize: "13px", wordBreak: "break-all" }}>
691
+ {selectedElement.id}
692
+ </div>
693
+ </div>
694
+ {!("source" in selectedElement) && (
695
+ <>
696
+ <div style={{ marginBottom: "12px" }}>
697
+ <label style={{ display: "block", color: "#c99aa3", fontSize: "12px", marginBottom: "4px" }}>
698
+ Label
699
+ </label>
700
+ <input
701
+ type="text"
702
+ value={String(selectedElement.data.label ?? "")}
703
+ readOnly={!isEditableEntityType(selectedElement.data.entityType)}
704
+ onChange={(e) => {
705
+ if (!isEditableEntityType(selectedElement.data.entityType)) return;
706
+ setNodes((nds) =>
707
+ nds.map((n) =>
708
+ n.id === selectedElement.id
709
+ ? { ...n, data: { ...n.data, label: e.target.value } }
710
+ : n
711
+ )
712
+ );
713
+ setDraftDiff((prev) => ({
714
+ ...prev,
715
+ ...(selectedElement.data.entityType === "property"
716
+ ? {
717
+ modified_properties: {
718
+ ...prev.modified_properties,
719
+ [selectedElement.id]: { label: e.target.value },
720
+ },
721
+ }
722
+ : {
723
+ modified_classes: {
724
+ ...prev.modified_classes,
725
+ [selectedElement.id]: { label: e.target.value },
726
+ },
727
+ }),
728
+ }));
729
+ }}
730
+ style={{
731
+ width: "100%",
732
+ padding: "8px",
733
+ borderRadius: "6px",
734
+ border: "1px solid rgba(255, 107, 129, 0.2)",
735
+ background: "rgba(6, 3, 5, 0.8)",
736
+ color: "#fff0f2",
737
+ fontSize: "13px",
738
+ }}
739
+ />
740
+ </div>
741
+ <div style={{ marginBottom: "12px" }}>
742
+ <label style={{ display: "block", color: "#c99aa3", fontSize: "12px", marginBottom: "4px" }}>
743
+ Type
744
+ </label>
745
+ <div style={{ color: "#fff0f2", fontSize: "13px" }}>
746
+ {selectedElement.data.type || "owl:Class"}
747
+ </div>
748
+ </div>
749
+ </>
750
+ )}
751
+ </div>
752
+ )}
753
+ </div>
754
+ </div>
755
+ );
756
+ }
757
+
758
+ const canvasMessageStyle: React.CSSProperties = {
759
+ position: "absolute",
760
+ left: "50%",
761
+ top: "50%",
762
+ transform: "translate(-50%, -50%)",
763
+ padding: "10px 14px",
764
+ borderRadius: "8px",
765
+ border: "1px solid rgba(255, 107, 129, 0.18)",
766
+ background: "rgba(6, 3, 5, 0.9)",
767
+ color: "#c99aa3",
768
+ fontSize: "13px",
769
+ pointerEvents: "none",
770
+ };