@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,771 @@
1
+ import type { CSSProperties } from "react";
2
+ import { Loader2 } from "lucide-react";
3
+ import { graph } from "../../store/graphStore";
4
+ import { GRAPH_THEME, withAlpha } from "./graphTheme";
5
+ import type { FocusedUnavailableReason, GraphSelectedNodeKind } from "./types";
6
+ import { focusedUnavailableReasonText } from "./graphViewCopy";
7
+ import { MarkdownContentViewer } from "./MarkdownContentViewer";
8
+ import type { MarkdownApplyResult } from "./markdownResourceClient";
9
+
10
+ export type LinkPrediction = {
11
+ target: string;
12
+ type: string;
13
+ label?: string;
14
+ score: number;
15
+ };
16
+
17
+ export type PathResponse = {
18
+ path: string[];
19
+ edge_ids?: string[];
20
+ total_weight: number;
21
+ hop_count: number;
22
+ distance_band: "direct" | "near" | "mid-range" | "distant";
23
+ // FR-1 distance intelligence enrichment
24
+ semantic_similarity?: number | null;
25
+ path_coherence_score?: number | null;
26
+ confidence_decay?: number | null;
27
+ bottleneck_node?: string | null;
28
+ alternative_path_count?: number;
29
+ interpretation?: string;
30
+ };
31
+
32
+ export interface GraphInspectorPanelProps {
33
+ nodeId: string;
34
+ inspectableNodeId?: string | null;
35
+ selectedNodeKind?: GraphSelectedNodeKind;
36
+ canActivateFocused?: boolean;
37
+ focusedUnavailableReason?: FocusedUnavailableReason | null;
38
+ predictions: LinkPrediction[];
39
+ predictionType: string;
40
+ onPredictionTypeChange: (value: string) => void;
41
+ onRunPredictions: () => void;
42
+ isRunningPredictions?: boolean;
43
+ pathTargetId: string;
44
+ onPathTargetChange: (value: string) => void;
45
+ onTracePath: () => void;
46
+ pathResult: PathResponse | null;
47
+ onDownloadProvenance: (format: "json" | "markdown") => void;
48
+ onFocusNode?: (nodeId: string) => void;
49
+ onMarkdownApplied?: (result: MarkdownApplyResult) => void;
50
+ onMarkdownDirtyChange?: (dirty: boolean) => void;
51
+ }
52
+
53
+ const PROVENANCE_KEYS = ["source", "source_url", "pmid", "pmids", "evidence", "provenance", "confidence"] as const;
54
+
55
+ function sourceAttribution(properties: Record<string, unknown>) {
56
+ return PROVENANCE_KEYS
57
+ .filter((key) => key in properties)
58
+ .map((key) => ({ key, value: properties[key] }));
59
+ }
60
+
61
+ /* ─── Path Distance Intelligence Panel ──────────────────────────── */
62
+
63
+ const BAND_COLORS: Record<string, string> = {
64
+ direct: "#3fb950",
65
+ near: "#ff8fa0",
66
+ "mid-range": "#e3b341",
67
+ distant: "#ff7b72",
68
+ };
69
+
70
+ function PathDistanceIntelPanel({ result }: { result: PathResponse }) {
71
+ const bandColor = BAND_COLORS[result.distance_band] ?? "#b08b93";
72
+ const hasMetrics =
73
+ result.confidence_decay != null ||
74
+ result.semantic_similarity != null ||
75
+ result.path_coherence_score != null ||
76
+ result.bottleneck_node != null;
77
+
78
+ return (
79
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
80
+ {/* distance band + alt paths */}
81
+ <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
82
+ <span
83
+ style={{
84
+ padding: "3px 8px",
85
+ borderRadius: 999,
86
+ background: withAlpha(bandColor, 0.14),
87
+ border: `1px solid ${withAlpha(bandColor, 0.3)}`,
88
+ color: bandColor,
89
+ fontSize: 11,
90
+ fontWeight: 700,
91
+ }}
92
+ >
93
+ {result.distance_band} · {result.hop_count} hop{result.hop_count !== 1 ? "s" : ""}
94
+ </span>
95
+ {(result.alternative_path_count ?? 0) > 0 && (
96
+ <span style={subtleChipStyle}>{result.alternative_path_count} alt path{result.alternative_path_count !== 1 ? "s" : ""}</span>
97
+ )}
98
+ </div>
99
+
100
+ {/* metric grid */}
101
+ {hasMetrics && <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
102
+ {result.confidence_decay != null && (
103
+ <div style={metricCardStyle}>
104
+ <div style={metricLabelStyle}>Confidence Decay</div>
105
+ <div
106
+ style={{
107
+ ...metricValueStyle,
108
+ color: result.confidence_decay > 0.6 ? "#3fb950" : result.confidence_decay > 0.3 ? "#e3b341" : "#ff7b72",
109
+ }}
110
+ >
111
+ {(result.confidence_decay * 100).toFixed(1)}%
112
+ </div>
113
+ <div style={metricBarTrackStyle}>
114
+ <div
115
+ style={{
116
+ ...metricBarFillStyle,
117
+ width: `${result.confidence_decay * 100}%`,
118
+ background:
119
+ result.confidence_decay > 0.6 ? "#3fb950" : result.confidence_decay > 0.3 ? "#e3b341" : "#ff7b72",
120
+ }}
121
+ />
122
+ </div>
123
+ </div>
124
+ )}
125
+ {result.semantic_similarity != null && (
126
+ <div style={metricCardStyle}>
127
+ <div style={metricLabelStyle}>Semantic Sim.</div>
128
+ <div style={{ ...metricValueStyle, color: "#ff8fa0" }}>
129
+ {(result.semantic_similarity * 100).toFixed(1)}%
130
+ </div>
131
+ <div style={metricBarTrackStyle}>
132
+ <div style={{ ...metricBarFillStyle, width: `${result.semantic_similarity * 100}%`, background: "#ff8fa0" }} />
133
+ </div>
134
+ </div>
135
+ )}
136
+ {result.path_coherence_score != null && (
137
+ <div style={metricCardStyle}>
138
+ <div style={metricLabelStyle}>Path Coherence</div>
139
+ <div style={{ ...metricValueStyle, color: "#a5d6a7" }}>
140
+ {(result.path_coherence_score * 100).toFixed(1)}%
141
+ </div>
142
+ </div>
143
+ )}
144
+ {result.bottleneck_node && (
145
+ <div style={metricCardStyle}>
146
+ <div style={metricLabelStyle}>Bottleneck</div>
147
+ <div
148
+ style={{
149
+ ...metricValueStyle,
150
+ color: "#e3b341",
151
+ fontSize: 11,
152
+ overflow: "hidden",
153
+ textOverflow: "ellipsis",
154
+ whiteSpace: "nowrap",
155
+ }}
156
+ title={result.bottleneck_node}
157
+ >
158
+ {getNodeLabel(result.bottleneck_node)}
159
+ </div>
160
+ </div>
161
+ )}
162
+ </div>}
163
+
164
+ {/* interpretation */}
165
+ {result.interpretation && (
166
+ <div
167
+ style={{
168
+ padding: "8px 10px",
169
+ background: "rgba(255,45,75,0.06)",
170
+ borderRadius: 8,
171
+ border: "1px solid rgba(255,45,75,0.14)",
172
+ color: "#a0b4cc",
173
+ fontSize: 12,
174
+ lineHeight: 1.5,
175
+ }}
176
+ >
177
+ {result.interpretation}
178
+ </div>
179
+ )}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ /* ─── Path Flow Visualizer ──────────────────────────────────────── */
185
+
186
+ function getNodeLabel(nodeId: string): string {
187
+ if (!graph.hasNode(nodeId)) return nodeId;
188
+ const attrs = graph.getNodeAttributes(nodeId) as { label?: string; content?: string };
189
+ return String(attrs.label ?? attrs.content ?? nodeId);
190
+ }
191
+
192
+ function getEdgeLabelBetween(sourceId: string, targetId: string, edgeIds?: string[]): string {
193
+ // Try to find the specific edge from edgeIds first
194
+ if (edgeIds) {
195
+ for (const edgeId of edgeIds) {
196
+ if (graph.hasEdge(edgeId)) {
197
+ const [src, tgt] = graph.extremities(edgeId);
198
+ if ((src === sourceId && tgt === targetId) || (src === targetId && tgt === sourceId)) {
199
+ const attrs = graph.getEdgeAttributes(edgeId) as { edgeType?: string };
200
+ return attrs.edgeType ?? "→";
201
+ }
202
+ }
203
+ }
204
+ }
205
+ // Fallback: find any edge between the pair
206
+ if (graph.hasNode(sourceId) && graph.hasNode(targetId)) {
207
+ let label = "→";
208
+ graph.forEachEdge(sourceId, targetId, (_edgeId, attrs) => {
209
+ const edgeAttrs = attrs as { edgeType?: string };
210
+ if (edgeAttrs.edgeType) label = edgeAttrs.edgeType;
211
+ });
212
+ return label;
213
+ }
214
+ return "→";
215
+ }
216
+
217
+ function PathFlowViz({
218
+ path,
219
+ edgeIds,
220
+ totalWeight,
221
+ bottleneckNodeId,
222
+ onFocusNode,
223
+ }: {
224
+ path: string[];
225
+ edgeIds?: string[];
226
+ totalWeight: number;
227
+ bottleneckNodeId?: string | null;
228
+ onFocusNode?: (nodeId: string) => void;
229
+ }) {
230
+ if (path.length === 0) {
231
+ return <div style={emptyTextStyle}>No path found between the selected nodes.</div>;
232
+ }
233
+
234
+ return (
235
+ <div>
236
+ {/* Horizontal scrollable chip flow */}
237
+ <div style={pathFlowContainerStyle}>
238
+ {path.map((nodeId, index) => {
239
+ const label = getNodeLabel(nodeId);
240
+ const edgeLabel =
241
+ index < path.length - 1
242
+ ? getEdgeLabelBetween(nodeId, path[index + 1], edgeIds)
243
+ : null;
244
+
245
+ return (
246
+ <div key={`${nodeId}-${index}`} style={{ display: "contents" }}>
247
+ {/* Node chip */}
248
+ <button
249
+ onClick={() => onFocusNode?.(nodeId)}
250
+ title={nodeId === bottleneckNodeId ? `Bottleneck: ${nodeId}` : `Focus: ${nodeId}`}
251
+ style={{
252
+ ...pathNodeChipStyle,
253
+ cursor: onFocusNode ? "pointer" : "default",
254
+ ...(nodeId === bottleneckNodeId
255
+ ? { border: "1px solid rgba(227,179,65,0.5)", background: "rgba(227,179,65,0.12)" }
256
+ : {}),
257
+ }}
258
+ >
259
+ <span style={pathNodeIndexStyle}>{index + 1}</span>
260
+ <span style={{ maxWidth: 120, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
261
+ {label}
262
+ </span>
263
+ </button>
264
+
265
+ {/* Edge connector */}
266
+ {edgeLabel !== null ? (
267
+ <div style={pathEdgeConnectorStyle}>
268
+ <div style={{ width: 16, height: 1, background: "rgba(255,45,75,0.3)" }} />
269
+ <span style={pathEdgeLabelStyle}>{edgeLabel}</span>
270
+ <div style={{ display: "flex", alignItems: "center" }}>
271
+ <div style={{ width: 12, height: 1, background: "rgba(255,45,75,0.3)" }} />
272
+ <div style={{ width: 0, height: 0, borderTop: "4px solid transparent", borderBottom: "4px solid transparent", borderLeft: "5px solid rgba(255,45,75,0.4)" }} />
273
+ </div>
274
+ </div>
275
+ ) : null}
276
+ </div>
277
+ );
278
+ })}
279
+ </div>
280
+
281
+ {/* Weight badge */}
282
+ <div style={{ marginTop: 8, display: "flex", alignItems: "center", gap: 8 }}>
283
+ <span style={{ color: "#9c7a82", fontSize: 11 }}>Total weight:</span>
284
+ <span style={{ color: "#ff8fa0", fontSize: 12, fontWeight: 700 }}>{totalWeight.toFixed(3)}</span>
285
+ <span style={{ color: "#9c7a82", fontSize: 11 }}>·</span>
286
+ <span style={{ color: "#9c7a82", fontSize: 11 }}>{path.length} hops</span>
287
+ </div>
288
+ </div>
289
+ );
290
+ }
291
+
292
+ /* ─── Main Panel ─────────────────────────────────────────────────── */
293
+
294
+ export function GraphInspectorPanel({
295
+ nodeId,
296
+ inspectableNodeId,
297
+ selectedNodeKind = "none",
298
+ canActivateFocused = false,
299
+ focusedUnavailableReason = null,
300
+ predictions,
301
+ predictionType,
302
+ onPredictionTypeChange,
303
+ onRunPredictions,
304
+ isRunningPredictions = false,
305
+ pathTargetId,
306
+ onPathTargetChange,
307
+ onTracePath,
308
+ pathResult,
309
+ onDownloadProvenance,
310
+ onFocusNode,
311
+ onMarkdownApplied,
312
+ onMarkdownDirtyChange,
313
+ }: GraphInspectorPanelProps) {
314
+ if (!nodeId) {
315
+ return (
316
+ <div style={{ padding: 32, textAlign: "center", display: "flex", flexDirection: "column", alignItems: "center", gap: 12, marginTop: 32 }}>
317
+ <div style={{ width: 40, height: 40, borderRadius: "50%", background: "rgba(98, 226, 205, 0.07)", border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
318
+ <div style={{ width: 14, height: 14, borderRadius: "50%", background: GRAPH_THEME.ui.timeline.playheadSoft }} />
319
+ </div>
320
+ <p style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 14, margin: 0, lineHeight: 1.6 }}>
321
+ Search for a node or click one in the canvas to inspect its properties.
322
+ </p>
323
+ </div>
324
+ );
325
+ }
326
+
327
+ const resolvedNodeId = inspectableNodeId && graph.hasNode(inspectableNodeId) ? inspectableNodeId : null;
328
+ const directlyInspectable = graph.hasNode(nodeId);
329
+ const effectiveNodeId = directlyInspectable ? nodeId : resolvedNodeId;
330
+ const actionNodeId = directlyInspectable ? nodeId : resolvedNodeId;
331
+ const groupedDisplaySelection = selectedNodeKind === "grouped" && !directlyInspectable;
332
+
333
+ if (!effectiveNodeId) {
334
+ return (
335
+ <aside style={{ padding: 24, display: "flex", flexDirection: "column", gap: 16 }}>
336
+ <div style={{ borderBottom: `1px solid ${GRAPH_THEME.ui.surface.divider}`, paddingBottom: 16 }}>
337
+ <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
338
+ <span style={{ background: GRAPH_THEME.ui.timeline.playhead, boxShadow: "0 0 10px rgba(98, 226, 205, 0.34)", width: 8, height: 8, borderRadius: "50%" }} />
339
+ <span style={{ color: GRAPH_THEME.ui.timeline.playhead, fontSize: 12, fontWeight: 700 }}>Selection</span>
340
+ </div>
341
+ <h3 style={{ margin: 0, color: GRAPH_THEME.ui.text.strong, fontSize: 20, fontWeight: 700, wordBreak: "break-word" }}>
342
+ {nodeId}
343
+ </h3>
344
+ <div style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 12, marginTop: 6, fontFamily: "monospace", wordBreak: "break-all" }}>{nodeId}</div>
345
+ </div>
346
+ <div style={groupedSelectionNoticeStyle}>
347
+ <div style={{ color: GRAPH_THEME.ui.text.strong, fontWeight: 600, marginBottom: 6 }}>Selected item is not directly inspectable in the current graph.</div>
348
+ <div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, lineHeight: 1.6 }}>
349
+ {canActivateFocused
350
+ ? "Use Focus to resolve this grouped selection to its canonical node."
351
+ : focusedUnavailableReasonText(focusedUnavailableReason)}
352
+ </div>
353
+ </div>
354
+ </aside>
355
+ );
356
+ }
357
+
358
+ const attributes = graph.getNodeAttributes(effectiveNodeId) as {
359
+ color?: string;
360
+ content?: string;
361
+ label?: string;
362
+ nodeType?: string;
363
+ valid_from?: string | null;
364
+ valid_until?: string | null;
365
+ properties?: Record<string, unknown>;
366
+ };
367
+ const properties = attributes?.properties ?? {};
368
+ const attribution = sourceAttribution(properties);
369
+ const accentColor = attributes?.color || "#ff2d4b";
370
+ const propertyEntries = Object.entries(properties).filter(
371
+ ([key]) =>
372
+ !["x","y","valid_from","valid_until","content","source","source_url","pmid","pmids","evidence","provenance","confidence"].includes(key),
373
+ );
374
+ const nodeContent = (typeof attributes?.content === "string" && attributes.content)
375
+ ? attributes.content
376
+ : (typeof properties.content === "string" && properties.content)
377
+ ? properties.content
378
+ : "";
379
+
380
+ return (
381
+ <aside style={{ padding: 24, display: "flex", flexDirection: "column", gap: 18 }}>
382
+ {/* Node identity */}
383
+ <div style={{ borderBottom: `1px solid ${GRAPH_THEME.ui.surface.divider}`, paddingBottom: 16 }}>
384
+ <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
385
+ <span style={{ background: accentColor, boxShadow: `0 0 10px ${accentColor}`, width: 8, height: 8, borderRadius: "50%" }} />
386
+ <span style={{ color: accentColor, fontSize: 12, fontWeight: 700 }}>
387
+ {groupedDisplaySelection ? "Grouped Selection" : (attributes?.nodeType || "Entity")}
388
+ </span>
389
+ </div>
390
+ <h3 style={{ margin: 0, color: GRAPH_THEME.ui.text.strong, fontSize: 20, fontWeight: 700, wordBreak: "break-word" }}>
391
+ {String(attributes?.label ?? effectiveNodeId)}
392
+ </h3>
393
+ <div style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 12, marginTop: 6, fontFamily: "monospace", wordBreak: "break-all" }}>
394
+ {groupedDisplaySelection ? nodeId : effectiveNodeId}
395
+ </div>
396
+ {groupedDisplaySelection ? (
397
+ <div style={groupedSelectionNoticeStyle}>
398
+ <div style={{ color: GRAPH_THEME.ui.text.strong, fontWeight: 600, marginBottom: 6 }}>This grouped item stays display-level until you explicitly enter Focused mode.</div>
399
+ <div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, lineHeight: 1.6 }}>
400
+ {canActivateFocused
401
+ ? `Canonical node available: ${effectiveNodeId}`
402
+ : focusedUnavailableReasonText(focusedUnavailableReason)}
403
+ </div>
404
+ </div>
405
+ ) : null}
406
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
407
+ {attributes?.valid_from || attributes?.valid_until ? (
408
+ <span style={subtleChipStyle}>temporal</span>
409
+ ) : null}
410
+ {attribution.length ? <span style={subtleChipStyle}>{attribution.length} source fields</span> : null}
411
+ {predictions.length ? <span style={subtleChipStyle}>{predictions.length} candidate links</span> : null}
412
+ </div>
413
+ </div>
414
+
415
+ {/* Temporal bounds */}
416
+ {(attributes?.valid_from || attributes?.valid_until) ? (
417
+ <div style={{ padding: "10px 12px", background: "rgba(233, 196, 122, 0.075)", border: "1px solid rgba(233, 196, 122, 0.22)", borderRadius: 8, fontSize: 12, color: GRAPH_THEME.palette.accent.selected, fontFamily: "monospace" }}>
418
+ {attributes?.valid_from ? <div>from: {attributes.valid_from}</div> : null}
419
+ {attributes?.valid_until ? <div>until: {attributes.valid_until}</div> : null}
420
+ </div>
421
+ ) : null}
422
+
423
+ {/* Canonical nodes remain editable even when their current body is empty. */}
424
+ <details className="node-panel-collapse" open>
425
+ <summary className="node-panel-summary">Content</summary>
426
+ <div className="node-panel-body" style={{ marginTop: 8 }}>
427
+ <MarkdownContentViewer
428
+ content={nodeContent}
429
+ resource={{ kind: "context-node", id: effectiveNodeId }}
430
+ onApplied={onMarkdownApplied}
431
+ onDirtyChange={onMarkdownDirtyChange}
432
+ />
433
+ </div>
434
+ </details>
435
+
436
+ {/* Actions */}
437
+ <section style={sectionStyle}>
438
+ <div style={sectionTitleStyle}>Actions</div>
439
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
440
+ <button
441
+ style={{ ...actionButtonStyle, width: "100%", justifyContent: "center", opacity: isRunningPredictions ? 0.7 : 1 }}
442
+ onClick={onRunPredictions}
443
+ disabled={isRunningPredictions || !actionNodeId}
444
+ >
445
+ {isRunningPredictions ? (
446
+ <Loader2 size={14} className="animate-spin" style={{ marginRight: 6 }} />
447
+ ) : null}
448
+ {isRunningPredictions ? "Running…" : "Run Link Prediction"}
449
+ </button>
450
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
451
+ <button style={secondaryActionButtonStyle} onClick={() => onDownloadProvenance("json")} disabled={!actionNodeId}>
452
+ Provenance JSON
453
+ </button>
454
+ <button style={secondaryActionButtonStyle} onClick={() => onDownloadProvenance("markdown")} disabled={!actionNodeId}>
455
+ Provenance MD
456
+ </button>
457
+ </div>
458
+ </div>
459
+ <input
460
+ value={predictionType}
461
+ onChange={(event) => onPredictionTypeChange(event.target.value)}
462
+ placeholder="Optional candidate type filter, e.g. disease"
463
+ style={inputStyle}
464
+ />
465
+ </section>
466
+
467
+ {/* Trace Path */}
468
+ <section style={sectionStyle}>
469
+ <div style={sectionTitleStyle}>Trace Path</div>
470
+ <input
471
+ value={pathTargetId}
472
+ onChange={(event) => onPathTargetChange(event.target.value)}
473
+ placeholder="Target node ID"
474
+ style={inputStyle}
475
+ />
476
+ <button style={actionButtonStyle} onClick={onTracePath} disabled={!actionNodeId}>Trace Causal Path</button>
477
+
478
+ {pathResult?.path?.length ? (
479
+ <>
480
+ <PathFlowViz
481
+ path={pathResult.path}
482
+ edgeIds={pathResult.edge_ids}
483
+ totalWeight={pathResult.total_weight}
484
+ bottleneckNodeId={pathResult.bottleneck_node}
485
+ onFocusNode={onFocusNode}
486
+ />
487
+ <PathDistanceIntelPanel result={pathResult} />
488
+ </>
489
+ ) : (
490
+ <div style={emptyTextStyle}>
491
+ Choose a target or click a candidate prediction to prepare a path trace.
492
+ </div>
493
+ )}
494
+ </section>
495
+
496
+ {/* Candidate Links */}
497
+ <details className="node-panel-collapse" open={predictions.length > 0}>
498
+ <summary className="node-panel-summary">Candidate Links</summary>
499
+ <div className="node-panel-body">
500
+ {predictions.length > 0 ? (
501
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
502
+ {predictions.map((prediction) => (
503
+ <button
504
+ key={`${prediction.target}-${prediction.type}`}
505
+ style={predictionCardStyle}
506
+ onClick={() => onPathTargetChange(prediction.target)}
507
+ >
508
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
509
+ <div>
510
+ <div style={{ color: GRAPH_THEME.ui.text.strong, fontWeight: 600 }}>{prediction.label || prediction.target}</div>
511
+ <div style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 12 }}>{prediction.type}</div>
512
+ </div>
513
+ <div style={{ flexShrink: 0 }}>
514
+ <div style={{
515
+ padding: "2px 7px",
516
+ borderRadius: 999,
517
+ fontSize: 10,
518
+ fontWeight: 700,
519
+ background: GRAPH_THEME.ui.timeline.playheadSoft,
520
+ border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
521
+ color: GRAPH_THEME.ui.timeline.playhead,
522
+ }}>
523
+ {(prediction.score * 100).toFixed(1)}%
524
+ </div>
525
+ </div>
526
+ </div>
527
+ </button>
528
+ ))}
529
+ </div>
530
+ ) : isRunningPredictions ? (
531
+ <div style={{ display: "flex", alignItems: "center", gap: 8, padding: 8, color: "#b08b93", fontSize: 12 }}>
532
+ <Loader2 size={13} className="animate-spin" />
533
+ <span>Computing candidate links…</span>
534
+ </div>
535
+ ) : (
536
+ <div style={emptyTextStyle}>Run link prediction to surface likely next-hop relationships.</div>
537
+ )}
538
+ </div>
539
+ </details>
540
+
541
+ {/* Source Attribution */}
542
+ <details className="node-panel-collapse">
543
+ <summary className="node-panel-summary">Source Attribution</summary>
544
+ <div className="node-panel-body">
545
+ {attribution.length ? (
546
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
547
+ {attribution.map(({ key, value }) => (
548
+ <div key={key} style={propertyCardStyle}>
549
+ <div style={{ color: GRAPH_THEME.ui.timeline.playhead, fontSize: 11, marginBottom: 4 }}>{key}</div>
550
+ <div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, wordBreak: "break-word" }}>
551
+ {typeof value === "object" ? JSON.stringify(value) : String(value)}
552
+ </div>
553
+ </div>
554
+ ))}
555
+ </div>
556
+ ) : (
557
+ <div style={emptyTextStyle}>No explicit attribution metadata was found on this node.</div>
558
+ )}
559
+ </div>
560
+ </details>
561
+
562
+ {/* Properties */}
563
+ <details className="node-panel-collapse">
564
+ <summary className="node-panel-summary">Properties</summary>
565
+ <div className="node-panel-body">
566
+ {propertyEntries.length ? (
567
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
568
+ {propertyEntries.map(([key, value]) => (
569
+ <div key={key} style={propertyCardStyle}>
570
+ <div style={{ color: GRAPH_THEME.ui.timeline.playhead, fontSize: 11, marginBottom: 4 }}>{key}</div>
571
+ <div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, wordBreak: "break-word" }}>
572
+ {typeof value === "object" ? JSON.stringify(value) : String(value)}
573
+ </div>
574
+ </div>
575
+ ))}
576
+ </div>
577
+ ) : (
578
+ <div style={emptyTextStyle}>No additional properties are attached to this node.</div>
579
+ )}
580
+ </div>
581
+ </details>
582
+ </aside>
583
+ );
584
+ }
585
+
586
+ /* ─── styles ─────────────────────────────────────────────────────── */
587
+
588
+ const inputStyle: CSSProperties = {
589
+ width: "100%",
590
+ background: GRAPH_THEME.ui.control.inputBg,
591
+ border: `1px solid ${GRAPH_THEME.ui.control.inputBorder}`,
592
+ color: GRAPH_THEME.ui.text.strong,
593
+ borderRadius: 12,
594
+ padding: "11px 13px",
595
+ fontSize: 13,
596
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,0.035)",
597
+ };
598
+
599
+ const groupedSelectionNoticeStyle: CSSProperties = {
600
+ marginTop: 12,
601
+ padding: "10px 12px",
602
+ background: "rgba(98, 226, 205, 0.07)",
603
+ border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
604
+ borderRadius: 12,
605
+ };
606
+
607
+ const actionButtonStyle: CSSProperties = {
608
+ background: GRAPH_THEME.ui.control.primaryBg,
609
+ color: GRAPH_THEME.ui.control.primaryText,
610
+ border: `1px solid ${GRAPH_THEME.ui.control.primaryBorder}`,
611
+ borderRadius: 12,
612
+ padding: "9px 12px",
613
+ cursor: "pointer",
614
+ fontWeight: 700,
615
+ fontSize: 12,
616
+ display: "inline-flex",
617
+ alignItems: "center",
618
+ justifyContent: "center",
619
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,0.07), 0 10px 24px rgba(0,0,0,0.18)",
620
+ };
621
+
622
+ const secondaryActionButtonStyle: CSSProperties = {
623
+ ...actionButtonStyle,
624
+ background: GRAPH_THEME.ui.control.defaultBg,
625
+ border: `1px solid ${GRAPH_THEME.ui.control.defaultBorder}`,
626
+ color: GRAPH_THEME.ui.control.defaultText,
627
+ fontWeight: 600,
628
+ };
629
+
630
+ const predictionCardStyle: CSSProperties = {
631
+ textAlign: "left",
632
+ padding: "10px 12px",
633
+ background: "rgba(255, 255, 255, 0.035)",
634
+ border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
635
+ borderRadius: 10,
636
+ cursor: "pointer",
637
+ width: "100%",
638
+ };
639
+
640
+ const propertyCardStyle: CSSProperties = {
641
+ background: "rgba(255, 255, 255, 0.028)",
642
+ padding: "10px 12px",
643
+ borderRadius: 10,
644
+ border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
645
+ };
646
+
647
+ const emptyTextStyle: CSSProperties = {
648
+ color: GRAPH_THEME.ui.text.muted,
649
+ fontSize: 12,
650
+ lineHeight: 1.5,
651
+ };
652
+
653
+ const subtleChipStyle: CSSProperties = {
654
+ background: "rgba(255, 255, 255, 0.04)",
655
+ color: GRAPH_THEME.ui.text.body,
656
+ padding: "4px 8px",
657
+ borderRadius: 999,
658
+ fontSize: 11,
659
+ border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
660
+ };
661
+
662
+ const sectionStyle: CSSProperties = {
663
+ display: "flex",
664
+ flexDirection: "column",
665
+ gap: 10,
666
+ padding: 14,
667
+ background: GRAPH_THEME.ui.surface.cardSubtle,
668
+ border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
669
+ borderRadius: 14,
670
+ };
671
+
672
+ const sectionTitleStyle: CSSProperties = {
673
+ color: GRAPH_THEME.ui.text.muted,
674
+ fontSize: 11,
675
+ fontWeight: 700,
676
+ textTransform: "uppercase",
677
+ letterSpacing: "0.08em",
678
+ };
679
+
680
+ const pathFlowContainerStyle: CSSProperties = {
681
+ display: "flex",
682
+ alignItems: "center",
683
+ gap: 0,
684
+ flexWrap: "wrap",
685
+ rowGap: 8,
686
+ };
687
+
688
+ const pathNodeChipStyle: CSSProperties = {
689
+ display: "inline-flex",
690
+ alignItems: "center",
691
+ gap: 6,
692
+ padding: "5px 10px",
693
+ borderRadius: 999,
694
+ background: "rgba(98, 226, 205, 0.08)",
695
+ border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
696
+ color: GRAPH_THEME.ui.text.strong,
697
+ fontSize: 12,
698
+ fontWeight: 600,
699
+ maxWidth: 160,
700
+ };
701
+
702
+ const pathNodeIndexStyle: CSSProperties = {
703
+ display: "inline-flex",
704
+ alignItems: "center",
705
+ justifyContent: "center",
706
+ width: 16,
707
+ height: 16,
708
+ borderRadius: "50%",
709
+ background: GRAPH_THEME.ui.timeline.playheadSoft,
710
+ color: GRAPH_THEME.ui.timeline.playhead,
711
+ fontSize: 9,
712
+ fontWeight: 800,
713
+ flexShrink: 0,
714
+ };
715
+
716
+ const pathEdgeConnectorStyle: CSSProperties = {
717
+ display: "inline-flex",
718
+ alignItems: "center",
719
+ gap: 2,
720
+ flexShrink: 0,
721
+ };
722
+
723
+ const pathEdgeLabelStyle: CSSProperties = {
724
+ fontSize: 9,
725
+ fontWeight: 700,
726
+ color: GRAPH_THEME.ui.text.subtle,
727
+ letterSpacing: "0.04em",
728
+ textTransform: "uppercase",
729
+ maxWidth: 70,
730
+ overflow: "hidden",
731
+ textOverflow: "ellipsis",
732
+ whiteSpace: "nowrap",
733
+ };
734
+
735
+ const metricCardStyle: CSSProperties = {
736
+ background: "rgba(0,0,0,0.18)",
737
+ borderRadius: 8,
738
+ padding: "8px 10px",
739
+ border: "1px solid rgba(255,255,255,0.05)",
740
+ display: "flex",
741
+ flexDirection: "column",
742
+ gap: 3,
743
+ };
744
+
745
+ const metricLabelStyle: CSSProperties = {
746
+ color: "rgba(255,45,75,0.65)",
747
+ fontSize: 10,
748
+ fontWeight: 700,
749
+ letterSpacing: "0.06em",
750
+ textTransform: "uppercase",
751
+ };
752
+
753
+ const metricValueStyle: CSSProperties = {
754
+ fontSize: 14,
755
+ fontWeight: 700,
756
+ color: "#ffeaed",
757
+ };
758
+
759
+ const metricBarTrackStyle: CSSProperties = {
760
+ height: 3,
761
+ borderRadius: 999,
762
+ background: "rgba(255,255,255,0.07)",
763
+ overflow: "hidden",
764
+ marginTop: 4,
765
+ };
766
+
767
+ const metricBarFillStyle: CSSProperties = {
768
+ height: "100%",
769
+ borderRadius: 999,
770
+ transition: "width 300ms ease",
771
+ };