@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.
- package/LICENSE +21 -0
- package/README.md +1674 -0
- package/explorer/README.md +282 -0
- package/explorer/src/App.css +0 -0
- package/explorer/src/App.tsx +2035 -0
- package/explorer/src/ErrorBoundary.tsx +112 -0
- package/explorer/src/ExploreWorkspaceTabs.tsx +29 -0
- package/explorer/src/assets/hero.png +0 -0
- package/explorer/src/assets/react.svg +1 -0
- package/explorer/src/assets/vite.svg +1 -0
- package/explorer/src/explorerCapabilities.ts +24 -0
- package/explorer/src/index.css +71 -0
- package/explorer/src/main.tsx +7 -0
- package/explorer/src/store/edgePairKeys.d.ts +2 -0
- package/explorer/src/store/edgePairKeys.js +7 -0
- package/explorer/src/store/graphStore.ts +183 -0
- package/explorer/src/store/registryStore.ts +78 -0
- package/explorer/src/types.d.ts +16 -0
- package/explorer/src/ui/primitives.css +414 -0
- package/explorer/src/ui/primitives.tsx +170 -0
- package/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +279 -0
- package/explorer/src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx +121 -0
- package/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +454 -0
- package/explorer/src/workspaces/EnrichWorkspace/RegistryTab.tsx +285 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx +2673 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +771 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +410 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +3584 -0
- package/explorer/src/workspaces/GraphWorkspace/MarkdownContentViewer.tsx +705 -0
- package/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx +55 -0
- package/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx +193 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts +25 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts +52 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts +779 -0
- package/explorer/src/workspaces/GraphWorkspace/graphColorLegend.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/graphConfig.ts +102 -0
- package/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts +34 -0
- package/explorer/src/workspaces/GraphWorkspace/graphLoading.ts +84 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts +775 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +2753 -0
- package/explorer/src/workspaces/GraphWorkspace/graphStructureLayer.ts +335 -0
- package/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +1078 -0
- package/explorer/src/workspaces/GraphWorkspace/graphViewCopy.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/localGraphTransition.ts +9 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownEditorState.ts +114 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownResourceClient.ts +103 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownUrlSafety.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/nodeMarkdownSync.ts +81 -0
- package/explorer/src/workspaces/GraphWorkspace/pluginRegistryPredicates.ts +24 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx +353 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx +522 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/index.ts +16 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx +120 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx +231 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx +142 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/types.ts +106 -0
- package/explorer/src/workspaces/GraphWorkspace/realtimeGraphAttributes.ts +50 -0
- package/explorer/src/workspaces/GraphWorkspace/scene.ts +79 -0
- package/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts +421 -0
- package/explorer/src/workspaces/GraphWorkspace/smallGraphLayout.ts +135 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts +31 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalScrubberBounds.ts +46 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalSnapshotGuards.ts +113 -0
- package/explorer/src/workspaces/GraphWorkspace/types.ts +395 -0
- package/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +746 -0
- package/explorer/src/workspaces/GraphWorkspace/useMarkdownEditor.ts +177 -0
- package/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx +196 -0
- package/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +177 -0
- package/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +291 -0
- package/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx +346 -0
- package/explorer/src/workspaces/MemoryWorkspace.tsx +464 -0
- package/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +437 -0
- package/explorer/src/workspaces/OntologyWorkspace/HealthTab.tsx +214 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyEditor.tsx +770 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyLoader.tsx +913 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +930 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +590 -0
- package/explorer/src/workspaces/OntologyWorkspace/ProposalReview.tsx +429 -0
- package/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +696 -0
- package/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +363 -0
- package/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +546 -0
- package/explorer/src/workspaces/OntologyWorkspace/api.ts +164 -0
- package/explorer/src/workspaces/OntologyWorkspace/index.tsx +101 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyEditorModel.ts +87 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyUrlState.ts +94 -0
- package/explorer/src/workspaces/OntologyWorkspace/types.ts +119 -0
- package/explorer/src/workspaces/ReasoningWorkspace.tsx +243 -0
- package/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +271 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx +97 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx +95 -0
- package/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx +116 -0
- package/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx +112 -0
- package/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx +70 -0
- package/explorer/src/workspaces/VocabularyWorkspace/queries.ts +52 -0
- package/explorer/src/workspaces/VocabularyWorkspace/types.ts +24 -0
- package/package.json +31 -0
- package/plugins/.claude-plugin/README.md +262 -0
- package/plugins/.claude-plugin/marketplace.json +20 -0
- package/plugins/.claude-plugin/plugin.json +33 -0
- package/plugins/.cline-plugin/README.md +32 -0
- package/plugins/.cline-plugin/marketplace.json +17 -0
- package/plugins/.cline-plugin/plugin.json +35 -0
- package/plugins/.codex-plugin/marketplace.json +21 -0
- package/plugins/.codex-plugin/plugin.json +35 -0
- package/plugins/.continue-plugin/README.md +34 -0
- package/plugins/.continue-plugin/marketplace.json +17 -0
- package/plugins/.continue-plugin/plugin.json +33 -0
- package/plugins/.cursor-plugin/marketplace.json +18 -0
- package/plugins/.cursor-plugin/plugin.json +32 -0
- package/plugins/.openclaw-plugin/README.md +62 -0
- package/plugins/.openclaw-plugin/marketplace.json +18 -0
- package/plugins/.openclaw-plugin/plugin.json +46 -0
- package/plugins/.pi-plugin/README.md +35 -0
- package/plugins/.vscode-plugin/README.md +48 -0
- package/plugins/.vscode-plugin/marketplace.json +17 -0
- package/plugins/.vscode-plugin/plugin.json +35 -0
- package/plugins/.windsurf-plugin/README.md +39 -0
- package/plugins/.windsurf-plugin/marketplace.json +17 -0
- package/plugins/.windsurf-plugin/plugin.json +35 -0
- package/plugins/agents/decision-advisor.md +126 -0
- package/plugins/agents/explainability.md +129 -0
- package/plugins/agents/kg-assistant.md +73 -0
- package/plugins/hooks/hooks.json +12 -0
- package/plugins/skills/causal/SKILL.md +74 -0
- package/plugins/skills/change/SKILL.md +80 -0
- package/plugins/skills/decision/SKILL.md +197 -0
- package/plugins/skills/deduplicate/SKILL.md +41 -0
- package/plugins/skills/embed/SKILL.md +230 -0
- package/plugins/skills/explain/SKILL.md +44 -0
- package/plugins/skills/export/SKILL.md +67 -0
- package/plugins/skills/extract/SKILL.md +93 -0
- package/plugins/skills/ingest/SKILL.md +38 -0
- package/plugins/skills/ontology/SKILL.md +89 -0
- package/plugins/skills/policy/SKILL.md +66 -0
- package/plugins/skills/provenance/SKILL.md +70 -0
- package/plugins/skills/query/SKILL.md +84 -0
- package/plugins/skills/reason/SKILL.md +201 -0
- package/plugins/skills/temporal/SKILL.md +164 -0
- package/plugins/skills/validate/SKILL.md +228 -0
- 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
|
+
};
|