@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,243 @@
1
+ import { useState } from "react";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { BrainCircuit, Play, RotateCcw, CheckCircle2, AlertCircle, Zap, GitBranch, Info } from "lucide-react";
4
+
5
+ const SAMPLE_FACTS = `inhibits(Metformin, mTOR)
6
+ causes(mTOR, Neurodegeneration)
7
+ treats(Metformin, Diabetes)`;
8
+
9
+ const SAMPLE_RULE = `IF inhibits(Metformin, mTOR) AND causes(mTOR, Neurodegeneration) THEN candidate(Metformin, Alzheimer's)`;
10
+
11
+ const TEMPLATES = [
12
+ {
13
+ label: "Drug Candidate",
14
+ facts: `inhibits(Metformin, mTOR)\ncauses(mTOR, Neurodegeneration)\ntreats(Metformin, Diabetes)`,
15
+ rule: `IF inhibits(Metformin, mTOR) AND causes(mTOR, Neurodegeneration) THEN candidate(Metformin, Alzheimer's)`,
16
+ },
17
+ {
18
+ label: "Gene → Disease",
19
+ facts: `expressed_in(BRCA1, Breast)\nmutated_in(BRCA1, Cancer)\nassociated_with(Breast, Cancer)`,
20
+ rule: `IF mutated_in(X, Cancer) AND expressed_in(X, Y) THEN risk_gene(X, Y)`,
21
+ },
22
+ {
23
+ label: "Pathway Activation",
24
+ facts: `activates(EGF, EGFR)\ndownstream_of(MAPK, EGFR)\ndownstream_of(AKT, EGFR)`,
25
+ rule: `IF activates(X, EGFR) AND downstream_of(Y, EGFR) THEN activates(X, Y)`,
26
+ },
27
+ ];
28
+
29
+ export function ReasoningWorkspace() {
30
+ const queryClient = useQueryClient();
31
+ const [facts, setFacts] = useState(SAMPLE_FACTS);
32
+ const [rules, setRules] = useState(SAMPLE_RULE);
33
+ const [applyToGraph, setApplyToGraph] = useState(true);
34
+ const [result, setResult] = useState<{
35
+ inferred_facts?: string[];
36
+ rules_fired?: number;
37
+ added_edges?: number;
38
+ mutated?: boolean;
39
+ } | null>(null);
40
+ const [isRunning, setIsRunning] = useState(false);
41
+ const [error, setError] = useState("");
42
+
43
+ async function handleRun() {
44
+ setIsRunning(true);
45
+ setError("");
46
+ setResult(null);
47
+ try {
48
+ const response = await fetch("/api/reason", {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({
52
+ facts: facts.split(/\r?\n/).map((l) => l.trim()).filter(Boolean),
53
+ rules: rules.split(/\r?\n/).map((l) => l.trim()).filter(Boolean),
54
+ mode: "forward",
55
+ apply_to_graph: applyToGraph,
56
+ }),
57
+ });
58
+ const data = await response.json();
59
+ if (!response.ok) throw new Error(data.detail || `Status ${response.status}`);
60
+ if (response.status === 207) setError(data.message || "Warning: Partial success reasoning.");
61
+ setResult(data);
62
+ if (data.mutated) queryClient.invalidateQueries({ queryKey: ["graph", "full-load"] });
63
+ } catch (e) {
64
+ setError(e instanceof Error ? e.message : "Reasoning failed");
65
+ } finally {
66
+ setIsRunning(false);
67
+ }
68
+ }
69
+
70
+ function loadTemplate(t: (typeof TEMPLATES)[number]) {
71
+ setFacts(t.facts);
72
+ setRules(t.rule);
73
+ setResult(null);
74
+ setError("");
75
+ }
76
+
77
+ function handleReset() {
78
+ setFacts(SAMPLE_FACTS);
79
+ setRules(SAMPLE_RULE);
80
+ setResult(null);
81
+ setError("");
82
+ }
83
+
84
+ return (
85
+ <div className="ws-page" style={{ flexDirection: "row" }}>
86
+ {/* ── Left: Input panel ── */}
87
+ <div style={{ width: 480, flexShrink: 0, display: "flex", flexDirection: "column", borderRight: "1px solid var(--ws-border)", overflow: "hidden" }}>
88
+ {/* Header */}
89
+ <div style={{ padding: "18px 20px 14px", borderBottom: "1px solid var(--ws-border)", flexShrink: 0 }}>
90
+ <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 2 }}>
91
+ <div style={{ width: 32, height: 32, borderRadius: 10, background: "var(--ws-accent-soft)", border: "1px solid var(--ws-border-strong)", display: "grid", placeItems: "center", color: "var(--ws-accent)", flexShrink: 0 }}>
92
+ <BrainCircuit size={16} />
93
+ </div>
94
+ <div>
95
+ <div className="ws-eyebrow" style={{ marginBottom: 2 }}>Forward Chaining</div>
96
+ <div style={{ color: "var(--ws-text)", fontWeight: 700, fontSize: 15, lineHeight: 1 }}>Inference Engine</div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ {/* Templates */}
102
+ <div style={{ padding: "12px 16px 10px", borderBottom: "1px solid var(--ws-border)", flexShrink: 0 }}>
103
+ <div className="ws-eyebrow" style={{ marginBottom: 8 }}>Quick Templates</div>
104
+ <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
105
+ {TEMPLATES.map((t) => (
106
+ <button key={t.label} className="ws-btn ws-btn--ghost" style={{ padding: "5px 10px", fontSize: 11 }} onClick={() => loadTemplate(t)}>
107
+ {t.label}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ </div>
112
+
113
+ {/* Input area */}
114
+ <div className="ws-scroll ws-padded" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
115
+ <div>
116
+ <label className="ws-label">Facts</label>
117
+ <div className="ws-body" style={{ marginBottom: 8 }}>One fact per line using <code style={{ color: "var(--ws-accent)", fontSize: 11 }}>predicate(subject, object)</code> form.</div>
118
+ <textarea
119
+ className="ws-textarea"
120
+ value={facts}
121
+ onChange={(e) => setFacts(e.target.value)}
122
+ rows={6}
123
+ spellCheck={false}
124
+ />
125
+ </div>
126
+
127
+ <div>
128
+ <label className="ws-label">Rules</label>
129
+ <div className="ws-body" style={{ marginBottom: 8 }}>Use <code style={{ color: "var(--ws-amber)", fontSize: 11 }}>IF … AND … THEN …</code> syntax. Falls back to internal matcher if the reasoning server is unavailable.</div>
130
+ <textarea
131
+ className="ws-textarea"
132
+ value={rules}
133
+ onChange={(e) => setRules(e.target.value)}
134
+ rows={5}
135
+ spellCheck={false}
136
+ />
137
+ </div>
138
+
139
+ {/* Apply toggle */}
140
+ <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", padding: "10px 12px", borderRadius: "var(--ws-radius-sm)", border: "1px solid var(--ws-border)", background: applyToGraph ? "var(--ws-green-soft)" : "var(--ws-surface)" }}>
141
+ <div style={{ position: "relative", width: 36, height: 20, flexShrink: 0 }}>
142
+ <input
143
+ type="checkbox"
144
+ checked={applyToGraph}
145
+ onChange={(e) => setApplyToGraph(e.target.checked)}
146
+ style={{ opacity: 0, position: "absolute", inset: 0, cursor: "pointer", margin: 0 }}
147
+ />
148
+ <div style={{ position: "absolute", inset: 0, borderRadius: 999, background: applyToGraph ? "var(--ws-green)" : "rgba(255,255,255,0.12)", transition: "background 180ms ease" }} />
149
+ <div style={{ position: "absolute", top: 3, left: applyToGraph ? 19 : 3, width: 14, height: 14, borderRadius: 999, background: "#fff", transition: "left 180ms ease", boxShadow: "0 1px 4px rgba(0,0,0,0.4)" }} />
150
+ </div>
151
+ <div>
152
+ <div style={{ fontSize: 13, fontWeight: 700, color: applyToGraph ? "#6ee7b7" : "var(--ws-text-muted)" }}>Write inferred facts to graph</div>
153
+ <div style={{ fontSize: 11, color: "var(--ws-text-dim)" }}>Inferred binary facts are added as edges</div>
154
+ </div>
155
+ </label>
156
+
157
+ {/* Actions */}
158
+ <div style={{ display: "flex", gap: 8 }}>
159
+ <button
160
+ className="ws-btn ws-btn--primary"
161
+ onClick={handleRun}
162
+ disabled={isRunning}
163
+ style={{ flex: 1, justifyContent: "center" }}
164
+ >
165
+ {isRunning
166
+ ? <><span className="ws-spin" style={{ display: "inline-block" }}><Zap size={15} /></span>Running…</>
167
+ : <><Play size={14} />Run Reasoning</>}
168
+ </button>
169
+ <button className="ws-btn ws-btn--ghost" onClick={handleReset} title="Reset to defaults">
170
+ <RotateCcw size={14} />
171
+ </button>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ {/* ── Right: Results panel ── */}
177
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
178
+ <div style={{ padding: "18px 24px 14px", borderBottom: "1px solid var(--ws-border)", flexShrink: 0, display: "flex", alignItems: "center", gap: 10 }}>
179
+ <div style={{ fontSize: 15, fontWeight: 700, color: "var(--ws-text)" }}>Inference Results</div>
180
+ {result && !isRunning && (
181
+ <div style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
182
+ <span className="ws-pill ws-pill--accent">
183
+ <Zap size={9} /> {result.rules_fired ?? 0} rules fired
184
+ </span>
185
+ <span className="ws-pill ws-pill--green">
186
+ <GitBranch size={9} /> {result.added_edges ?? 0} edges added
187
+ </span>
188
+ {result.mutated
189
+ ? <span className="ws-pill ws-pill--green">graph updated</span>
190
+ : <span className="ws-pill ws-pill--mono">preview only</span>}
191
+ </div>
192
+ )}
193
+ </div>
194
+
195
+ <div className="ws-scroll ws-padded" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
196
+ {error && (
197
+ <div className="ws-animate-in" style={{ display: "flex", gap: 10, padding: "12px 14px", borderRadius: "var(--ws-radius-sm)", background: "var(--ws-red-soft)", border: "1px solid rgba(255,123,114,0.28)", color: "#fca5a5", fontSize: 13 }}>
198
+ <AlertCircle size={16} style={{ flexShrink: 0, marginTop: 1 }} />
199
+ <div>{error}</div>
200
+ </div>
201
+ )}
202
+
203
+ {isRunning && (
204
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
205
+ {[1,2,3,4].map((i) => <div key={i} className="ws-skeleton" style={{ height: 52 }} />)}
206
+ </div>
207
+ )}
208
+
209
+ {result && !isRunning && (
210
+ <div className="ws-animate-in">
211
+ {(result.inferred_facts ?? []).length === 0 ? (
212
+ <div className="ws-empty">
213
+ <div className="ws-empty-icon"><CheckCircle2 size={32} /></div>
214
+ <div className="ws-empty-title">Reasoning complete</div>
215
+ <div className="ws-empty-body">No new facts were inferred from the current rule set and facts.</div>
216
+ </div>
217
+ ) : (
218
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
219
+ {result.inferred_facts!.map((fact, i) => (
220
+ <div key={`${fact}-${i}`} style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "12px 14px", borderRadius: "var(--ws-radius-sm)", background: "var(--ws-surface)", border: "1px solid var(--ws-border)" }}>
221
+ <div style={{ width: 20, height: 20, borderRadius: 6, background: "var(--ws-green-soft)", border: "1px solid rgba(76,195,138,0.28)", display: "grid", placeItems: "center", flexShrink: 0, marginTop: 1 }}>
222
+ <CheckCircle2 size={11} color="var(--ws-green)" />
223
+ </div>
224
+ <code style={{ fontFamily: "'JetBrains Mono','Fira Code',monospace", fontSize: 12, color: "var(--ws-text)", lineHeight: 1.6, wordBreak: "break-all" }}>{fact}</code>
225
+ </div>
226
+ ))}
227
+ </div>
228
+ )}
229
+ </div>
230
+ )}
231
+
232
+ {!result && !isRunning && !error && (
233
+ <div className="ws-empty">
234
+ <div className="ws-empty-icon"><Info size={32} /></div>
235
+ <div className="ws-empty-title">Ready to reason</div>
236
+ <div className="ws-empty-body">Enter facts and rules on the left, then click Run Reasoning to see inferred statements here.</div>
237
+ </div>
238
+ )}
239
+ </div>
240
+ </div>
241
+ </div>
242
+ );
243
+ }
@@ -0,0 +1,271 @@
1
+ import { useState, useRef } from "react";
2
+ import Editor, { useMonaco } from "@monaco-editor/react";
3
+ import { Play, Copy, Download, Table2, AlertCircle, FileCode2 } from "lucide-react";
4
+
5
+ const TEMPLATES: { label: string; query: string }[] = [
6
+ { label: "All triples", query: "SELECT ?s ?p ?o\nWHERE {\n ?s ?p ?o\n}\nLIMIT 20" },
7
+ { label: "Node types", query: "SELECT ?type (COUNT(?s) AS ?count)\nWHERE {\n ?s a ?type\n}\nGROUP BY ?type\nORDER BY DESC(?count)" },
8
+ { label: "Outgoing edges", query: "SELECT ?predicate ?object\nWHERE {\n <urn:node:example> ?predicate ?object\n}\nLIMIT 50" },
9
+ { label: "Path between", query: "SELECT ?mid ?p1 ?p2\nWHERE {\n <urn:node:a> ?p1 ?mid .\n ?mid ?p2 <urn:node:b>\n}\nLIMIT 20" },
10
+ ];
11
+
12
+ export function SparqlWorkspace() {
13
+ const monaco = useMonaco();
14
+ const editorRef = useRef<unknown>(null);
15
+ const [query, setQuery] = useState(TEMPLATES[0].query);
16
+ const [result, setResult] = useState<{ columns?: string[]; rows?: Record<string, string>[]; error?: string; error_line?: number } | null>(null);
17
+ const [isLoading, setIsLoading] = useState(false);
18
+ const [copyState, setCopyState] = useState(false);
19
+
20
+ function handleEditorWillMount(monacoIns: { languages: { getLanguages(): { id: string }[]; register(opts: { id: string }): void; setMonarchTokensProvider(id: string, p: unknown): void }; editor: { defineTheme(id: string, t: unknown): void } }) {
21
+ if (!monacoIns.languages.getLanguages().some((l) => l.id === "sparql")) {
22
+ monacoIns.languages.register({ id: "sparql" });
23
+ monacoIns.languages.setMonarchTokensProvider("sparql", {
24
+ keywords: ["SELECT", "WHERE", "LIMIT", "FILTER", "OPTIONAL", "PREFIX", "ORDER", "BY", "DESC", "ASC", "GROUP", "DISTINCT", "CONSTRUCT", "ASK", "DESCRIBE"],
25
+ tokenizer: {
26
+ root: [
27
+ [/[a-zA-Z_]\w*/, { cases: { "@keywords": "keyword", "@default": "identifier" } }],
28
+ [/[?$][a-zA-Z_]\w*/, "variable.name"],
29
+ [/<[^>]+>/, "string.uri"],
30
+ [/"[^"]*"/, "string"],
31
+ [/#.*/, "comment"],
32
+ [/[0-9]+(\.[0-9]+)?/, "number"],
33
+ ],
34
+ },
35
+ });
36
+ monacoIns.editor.defineTheme("sparql-dark", {
37
+ base: "vs-dark",
38
+ inherit: true,
39
+ rules: [
40
+ { token: "keyword", foreground: "58a6ff", fontStyle: "bold" },
41
+ { token: "variable.name", foreground: "a5d6ff" },
42
+ { token: "string.uri", foreground: "7ee787" },
43
+ { token: "string", foreground: "a5d6ff" },
44
+ { token: "comment", foreground: "4a6a85", fontStyle: "italic" },
45
+ { token: "number", foreground: "f2b66d" },
46
+ ],
47
+ colors: {
48
+ "editor.background": "#050c18",
49
+ "editor.lineHighlightBackground": "#0a1628",
50
+ "editorLineNumber.foreground": "#2a4060",
51
+ "editorCursor.foreground": "#ff2d4b",
52
+ "editor.selectionBackground": "#1e3a5a",
53
+ },
54
+ });
55
+ }
56
+ }
57
+
58
+ function handleEditorDidMount(editor: unknown) {
59
+ editorRef.current = editor;
60
+ }
61
+
62
+ async function handleRun() {
63
+ setIsLoading(true);
64
+ setResult(null);
65
+ if (monaco && editorRef.current) {
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ (monaco as any).editor.setModelMarkers((editorRef.current as any).getModel(), "sparql", []);
68
+ }
69
+ try {
70
+ const res = await fetch("/api/sparql", {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ body: JSON.stringify({ query }),
74
+ });
75
+ if (!res.headers.get("content-type")?.includes("application/json")) {
76
+ const text = await res.text();
77
+ throw new Error(`HTTP ${res.status}: ${text.substring(0, 100)}`);
78
+ }
79
+ const data = await res.json();
80
+ if (res.status === 207) {
81
+ data.error = data.message || "Warning: Partial success running query.";
82
+ }
83
+
84
+ if (data.error && data.error_line && monaco && editorRef.current) {
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ (monaco as any).editor.setModelMarkers((editorRef.current as any).getModel(), "sparql", [{
87
+ startLineNumber: data.error_line,
88
+ startColumn: data.error_column || 1,
89
+ endLineNumber: data.error_line,
90
+ endColumn: 100,
91
+ message: data.error,
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ severity: (monaco as any).MarkerSeverity.Error,
94
+ }]);
95
+ }
96
+ setResult(data);
97
+ } catch (e) {
98
+ const msg = e instanceof Error ? e.message : "Network error — could not reach the SPARQL endpoint.";
99
+ setResult({ error: msg });
100
+ } finally {
101
+ setIsLoading(false);
102
+ }
103
+ }
104
+
105
+ function handleCopyQuery() {
106
+ navigator.clipboard.writeText(query).then(() => {
107
+ setCopyState(true);
108
+ setTimeout(() => setCopyState(false), 1500);
109
+ }).catch(() => {
110
+ // Clipboard API unavailable (insecure context or denied) — no-op; query is visible in editor
111
+ });
112
+ }
113
+
114
+ function handleExportCSV() {
115
+ if (!result?.rows || !result?.columns) return;
116
+ const cols = result.columns;
117
+ const header = cols.join(",");
118
+ const rows = result.rows.map((r) => cols.map((c) => JSON.stringify(r[c] ?? "")).join(",")).join("\n");
119
+ const blob = new Blob([`${header}\n${rows}`], { type: "text/csv" });
120
+ const url = URL.createObjectURL(blob);
121
+ const a = document.createElement("a");
122
+ a.href = url;
123
+ a.download = "sparql_results.csv";
124
+ document.body.appendChild(a);
125
+ a.click();
126
+ document.body.removeChild(a);
127
+ URL.revokeObjectURL(url);
128
+ }
129
+
130
+ return (
131
+ <div className="ws-page">
132
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
133
+ {/* ── Toolbar ── */}
134
+ <div style={{ padding: "10px 16px", borderBottom: "1px solid var(--ws-border)", display: "flex", alignItems: "center", gap: 8, flexShrink: 0, background: "rgba(0,0,0,0.18)" }}>
135
+ <div style={{ display: "flex", gap: 6, flex: 1, flexWrap: "wrap" }}>
136
+ <span className="ws-eyebrow" style={{ alignSelf: "center", marginRight: 4 }}>Templates:</span>
137
+ {TEMPLATES.map((t) => (
138
+ <button
139
+ key={t.label}
140
+ className="ws-btn ws-btn--ghost"
141
+ style={{ padding: "4px 10px", fontSize: 11 }}
142
+ onClick={() => { setQuery(t.query); setResult(null); }}
143
+ >
144
+ {t.label}
145
+ </button>
146
+ ))}
147
+ </div>
148
+ <button className="ws-btn ws-btn--ghost" style={{ padding: "6px 10px" }} onClick={handleCopyQuery} title="Copy query">
149
+ <Copy size={13} />{copyState ? "Copied!" : "Copy"}
150
+ </button>
151
+ <button
152
+ className="ws-btn ws-btn--primary"
153
+ onClick={handleRun}
154
+ disabled={isLoading}
155
+ style={{ minWidth: 110, justifyContent: "center" }}
156
+ >
157
+ {isLoading
158
+ ? <><span className="ws-spin" style={{ display: "inline-block" }}><Play size={13} /></span>Running…</>
159
+ : <><Play size={13} />Run Query</>}
160
+ </button>
161
+ </div>
162
+
163
+ {/* ── Editor + Results split ── */}
164
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
165
+ {/* Editor */}
166
+ <div style={{ flex: "0 0 55%", minHeight: 0, borderBottom: "1px solid var(--ws-border)", position: "relative" }}>
167
+ <div style={{ position: "absolute", top: 8, right: 12, zIndex: 10, display: "flex", alignItems: "center", gap: 6 }}>
168
+ <span className="ws-pill ws-pill--mono"><FileCode2 size={9} />SPARQL</span>
169
+ </div>
170
+ <Editor
171
+ height="100%"
172
+ defaultLanguage="sparql"
173
+ theme="sparql-dark"
174
+ value={query}
175
+ onChange={(v) => setQuery(v || "")}
176
+ beforeMount={handleEditorWillMount}
177
+ onMount={handleEditorDidMount}
178
+ options={{
179
+ minimap: { enabled: false },
180
+ fontSize: 13,
181
+ fontFamily: "'JetBrains Mono','Fira Code',Consolas,monospace",
182
+ lineHeight: 22,
183
+ padding: { top: 16 },
184
+ scrollBeyondLastLine: false,
185
+ renderLineHighlight: "gutter",
186
+ wordWrap: "on",
187
+ }}
188
+ />
189
+ </div>
190
+
191
+ {/* Results */}
192
+ <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden", background: "rgba(0,0,0,0.12)" }}>
193
+ <div style={{ padding: "10px 16px", borderBottom: "1px solid var(--ws-border)", display: "flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
194
+ <div style={{ display: "flex", alignItems: "center", gap: 7, color: "var(--ws-text-muted)", fontSize: 13, fontWeight: 700 }}>
195
+ <Table2 size={14} />
196
+ Results
197
+ {result?.rows && <span className="ws-pill ws-pill--accent">{result.rows.length} rows</span>}
198
+ </div>
199
+ {result?.rows && result.rows.length > 0 && (
200
+ <button className="ws-btn ws-btn--ghost" style={{ marginLeft: "auto", padding: "4px 10px", fontSize: 11 }} onClick={handleExportCSV}>
201
+ <Download size={12} />Export CSV
202
+ </button>
203
+ )}
204
+ </div>
205
+
206
+ <div className="ws-scroll" style={{ flex: 1 }}>
207
+ {isLoading && (
208
+ <div style={{ padding: 24, display: "flex", flexDirection: "column", gap: 8 }}>
209
+ {[1, 2, 3].map((i) => <div key={i} className="ws-skeleton" style={{ height: 36 }} />)}
210
+ </div>
211
+ )}
212
+
213
+ {result?.error && !isLoading && (
214
+ <div className="ws-animate-in" style={{ margin: 16, display: "flex", gap: 10, padding: "12px 14px", borderRadius: "var(--ws-radius-sm)", background: "var(--ws-red-soft)", border: "1px solid rgba(255,123,114,0.28)", color: "#fca5a5", fontSize: 13 }}>
215
+ <AlertCircle size={16} style={{ flexShrink: 0, marginTop: 1 }} />
216
+ {result.error}
217
+ </div>
218
+ )}
219
+
220
+ {result?.rows && result?.columns && !isLoading && (
221
+ <div className="ws-animate-in" style={{ overflowX: "auto" }}>
222
+ {result.rows.length === 0 ? (
223
+ <div className="ws-empty">
224
+ <div className="ws-empty-title">No results</div>
225
+ <div className="ws-empty-body">The query returned 0 rows. Try a broader query or check your data.</div>
226
+ </div>
227
+ ) : (
228
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12, color: "var(--ws-text)" }}>
229
+ <thead>
230
+ <tr style={{ borderBottom: "1px solid var(--ws-border)", background: "rgba(0,0,0,0.2)" }}>
231
+ <th style={{ padding: "8px 14px", textAlign: "left", color: "var(--ws-text-dim)", fontFamily: "monospace", fontSize: 11, fontWeight: 700, letterSpacing: "0.06em", width: 40 }}>#</th>
232
+ {result.columns.map((c) => (
233
+ <th key={c} style={{ padding: "8px 14px", textAlign: "left", color: "var(--ws-text-muted)", fontFamily: "monospace", fontSize: 11, fontWeight: 700, letterSpacing: "0.06em" }}>?{c}</th>
234
+ ))}
235
+ </tr>
236
+ </thead>
237
+ <tbody>
238
+ {result.rows.map((r, i) => (
239
+ <tr key={i} style={{ borderBottom: "1px solid rgba(255,45,75,0.06)" }}>
240
+ <td style={{ padding: "7px 14px", color: "var(--ws-text-dim)", fontFamily: "monospace", fontSize: 11 }}>{i + 1}</td>
241
+ {(result.columns ?? []).map((c) => (
242
+ <td key={c} style={{ padding: "7px 14px", maxWidth: 300, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "monospace" }} title={String(r[c] ?? "")}>
243
+ {r[c] != null ? (
244
+ String(r[c]).startsWith("urn:") || String(r[c]).startsWith("http")
245
+ ? <span style={{ color: "#7ee787" }}>{String(r[c])}</span>
246
+ : String(r[c])
247
+ ) : <span style={{ color: "var(--ws-text-dim)", fontStyle: "italic" }}>null</span>}
248
+ </td>
249
+ ))}
250
+ </tr>
251
+ ))}
252
+ </tbody>
253
+ </table>
254
+ )}
255
+ </div>
256
+ )}
257
+
258
+ {!result && !isLoading && (
259
+ <div className="ws-empty">
260
+ <div className="ws-empty-icon"><Table2 size={28} /></div>
261
+ <div className="ws-empty-title">Run a query</div>
262
+ <div className="ws-empty-body">Write SPARQL above or pick a template, then click Run Query to see results here.</div>
263
+ </div>
264
+ )}
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ );
271
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * ConceptTree.tsx — react-arborist SKOS hierarchy viewer.
3
+ *
4
+ * Styled for the Palantir/Glassmorphism dark theme rather than
5
+ * the default light-mode colours.
6
+ */
7
+ import React from 'react';
8
+ import { Tree } from 'react-arborist';
9
+ import type { NodeRendererProps } from 'react-arborist';
10
+ import { ChevronRight, ChevronDown, Folder, FileText } from 'lucide-react';
11
+ import type { ConceptNode } from './types';
12
+
13
+ interface ConceptTreeProps {
14
+ data: ConceptNode[];
15
+ onSelectConcept: (concept: ConceptNode) => void;
16
+ }
17
+
18
+ export const ConceptTree: React.FC<ConceptTreeProps> = ({ data, onSelectConcept }) => {
19
+ return (
20
+ <div style={{
21
+ height: '100%', width: '100%',
22
+ backgroundColor: 'transparent',
23
+ overflow: 'hidden',
24
+ }}>
25
+ <Tree
26
+ data={data}
27
+ idAccessor="uri"
28
+ width="100%"
29
+ height={600}
30
+ indent={24}
31
+ rowHeight={36}
32
+ childrenAccessor="children"
33
+ >
34
+ {(nodeProps: NodeRendererProps<ConceptNode>) => {
35
+ const { node, style, dragHandle } = nodeProps;
36
+ const isFolder = node.children && node.children.length > 0;
37
+
38
+ return (
39
+ <div
40
+ ref={dragHandle}
41
+ onClick={() => {
42
+ node.toggle();
43
+ onSelectConcept(node.data);
44
+ }}
45
+ style={{
46
+ ...style,
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ padding: '0 8px',
50
+ cursor: 'pointer',
51
+ backgroundColor: node.isSelected
52
+ ? 'rgba(255,45,75,0.12)'
53
+ : 'transparent',
54
+ userSelect: 'none',
55
+ borderBottom: '1px solid rgba(255,255,255,0.04)',
56
+ transition: 'background 0.15s',
57
+ }}
58
+ onMouseEnter={(e) => {
59
+ if (!node.isSelected) {
60
+ (e.currentTarget as HTMLDivElement).style.background = 'rgba(255,45,75,0.06)';
61
+ }
62
+ }}
63
+ onMouseLeave={(e) => {
64
+ if (!node.isSelected) {
65
+ (e.currentTarget as HTMLDivElement).style.background = 'transparent';
66
+ }
67
+ }}
68
+ >
69
+ <span style={{ width: 20, display: 'flex', justifyContent: 'center' }}>
70
+ {isFolder ? (
71
+ node.isOpen
72
+ ? <ChevronDown size={14} color="#b08b93" />
73
+ : <ChevronRight size={14} color="#b08b93" />
74
+ ) : null}
75
+ </span>
76
+
77
+ <span style={{ marginRight: 8, display: 'flex', alignItems: 'center' }}>
78
+ {isFolder
79
+ ? <Folder size={14} color="#d2a8ff" />
80
+ : <FileText size={14} color="#484f58" />
81
+ }
82
+ </span>
83
+
84
+ <span style={{
85
+ fontSize: 13, color: '#e8ccd1',
86
+ whiteSpace: 'nowrap', overflow: 'hidden',
87
+ textOverflow: 'ellipsis',
88
+ }}>
89
+ {node.data.pref_label}
90
+ </span>
91
+ </div>
92
+ );
93
+ }}
94
+ </Tree>
95
+ </div>
96
+ );
97
+ };