@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,279 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Scale, Search, ArrowRight, Info } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
type OutcomeKind = "approved" | "rejected" | "deferred" | "pending" | string;
|
|
5
|
+
|
|
6
|
+
function outcomeColor(outcome: string) {
|
|
7
|
+
const o = (outcome ?? "").toLowerCase();
|
|
8
|
+
if (o.includes("approv") || o.includes("accept")) return { color: "#6ee7b7", bg: "var(--ws-green-soft)", border: "rgba(76,195,138,0.3)" };
|
|
9
|
+
if (o.includes("reject") || o.includes("denied") || o.includes("fail")) return { color: "#fca5a5", bg: "var(--ws-red-soft)", border: "rgba(255,123,114,0.3)" };
|
|
10
|
+
if (o.includes("defer") || o.includes("pending") || o.includes("review")) return { color: "#fbbf24", bg: "var(--ws-amber-soft)", border: "rgba(242,182,109,0.3)" };
|
|
11
|
+
return { color: "var(--ws-text-muted)", bg: "rgba(255,255,255,0.04)", border: "rgba(255,255,255,0.1)" };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function OutcomeBadge({ outcome }: { outcome: OutcomeKind }) {
|
|
15
|
+
const c = outcomeColor(outcome);
|
|
16
|
+
return (
|
|
17
|
+
<span style={{ display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: 10, fontWeight: 800, letterSpacing: "0.08em", textTransform: "uppercase", color: c.color, background: c.bg, border: `1px solid ${c.border}` }}>
|
|
18
|
+
{outcome || "unknown"}
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ChainStep {
|
|
24
|
+
id: string;
|
|
25
|
+
relationship: string;
|
|
26
|
+
content?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const NODE_ACCENTS = ["#ff2d4b","#4cc38a","#ff7a45","#c084fc","#ff7b72","#38bdf8","#a78bfa"];
|
|
32
|
+
|
|
33
|
+
function ChainNode({ step, index }: { step: ChainStep; index: number }) {
|
|
34
|
+
const accent = NODE_ACCENTS[index % NODE_ACCENTS.length];
|
|
35
|
+
return (
|
|
36
|
+
<div style={{ padding: "14px 16px", borderRadius: "var(--ws-radius)", background: "var(--ws-surface)", border: `1px solid ${accent}28`, borderLeft: `3px solid ${accent}`, position: "relative" }}>
|
|
37
|
+
{step.type && (
|
|
38
|
+
<div style={{ fontFamily: "monospace", fontSize: 10, fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase", color: accent, marginBottom: 5 }}>
|
|
39
|
+
{step.type}
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
<div style={{ color: "var(--ws-text)", fontSize: 13, fontWeight: 600 }}>{step.content || step.id}</div>
|
|
43
|
+
{step.id && step.id !== step.content && (
|
|
44
|
+
<div style={{ fontFamily: "monospace", fontSize: 10, color: "var(--ws-text-dim)", marginTop: 3 }}>{step.id}</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function RelEdge({ label }: { label: string }) {
|
|
51
|
+
return (
|
|
52
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 0, padding: "2px 0" }}>
|
|
53
|
+
<div style={{ width: 2, height: 10, background: "var(--ws-border)" }} />
|
|
54
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6, padding: "3px 10px", borderRadius: 999, background: "var(--ws-accent-soft)", border: "1px solid var(--ws-border-strong)", maxWidth: 260 }}>
|
|
55
|
+
<ArrowRight size={10} color="var(--ws-accent)" />
|
|
56
|
+
<span style={{ fontSize: 10, fontWeight: 700, color: "var(--ws-accent)", letterSpacing: "0.06em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{label}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div style={{ width: 2, height: 10, background: "var(--ws-border)" }} />
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function CausalFlow({ chain, loading }: { chain: ChainStep[]; loading: boolean }) {
|
|
64
|
+
if (loading) return (
|
|
65
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
66
|
+
{[1,2,3].map((i) => <div key={i} className="ws-skeleton" style={{ height: 68 }} />)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
if (!chain.length) return (
|
|
70
|
+
<div className="ws-empty">
|
|
71
|
+
<div className="ws-empty-icon"><Info size={28} /></div>
|
|
72
|
+
<div className="ws-empty-title">No chain steps</div>
|
|
73
|
+
<div className="ws-empty-body">No causal chain steps were found for this decision.</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
return (
|
|
77
|
+
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
78
|
+
{chain.map((step, i) => (
|
|
79
|
+
<div key={`${step.id}-${i}`}>
|
|
80
|
+
<ChainNode step={step} index={i} />
|
|
81
|
+
{i < chain.length - 1 && <RelEdge label={chain[i + 1]?.relationship || "→"} />}
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function DecisionWorkspace() {
|
|
89
|
+
const [decisions, setDecisions] = useState<{ decision_id: string; category?: string; outcome?: string }[]>([]);
|
|
90
|
+
const [selected, setSelected] = useState<{ decision_id: string; category?: string; outcome?: string } | null>(null);
|
|
91
|
+
const [chain, setChain] = useState<ChainStep[]>([]);
|
|
92
|
+
const [chainLoading, setChainLoading] = useState(false);
|
|
93
|
+
const [listLoading, setListLoading] = useState(true);
|
|
94
|
+
const [filter, setFilter] = useState("");
|
|
95
|
+
const [error, setError] = useState("");
|
|
96
|
+
|
|
97
|
+
// Tracks the active chain request so stale responses from rapid selections are ignored.
|
|
98
|
+
const chainCtrlRef = useRef<AbortController | null>(null);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const ctrl = new AbortController();
|
|
102
|
+
setListLoading(true);
|
|
103
|
+
setError("");
|
|
104
|
+
fetch("/api/decisions", { signal: ctrl.signal })
|
|
105
|
+
.then(async (r) => {
|
|
106
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
107
|
+
const data = await r.json();
|
|
108
|
+
if (r.status === 207) setError(data.message || "Warning: Partial success loading decisions.");
|
|
109
|
+
return data;
|
|
110
|
+
})
|
|
111
|
+
.then((data) => {
|
|
112
|
+
if (ctrl.signal.aborted) return;
|
|
113
|
+
setDecisions(data);
|
|
114
|
+
if (data.length > 0) void loadChain(data[0]);
|
|
115
|
+
})
|
|
116
|
+
.catch((e) => {
|
|
117
|
+
if (e?.name !== "AbortError") {
|
|
118
|
+
setError(e instanceof Error ? e.message : "Failed to load decisions.");
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.finally(() => {
|
|
122
|
+
if (!ctrl.signal.aborted) setListLoading(false);
|
|
123
|
+
});
|
|
124
|
+
return () => ctrl.abort();
|
|
125
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
// Cancel any in-flight chain request when the workspace unmounts.
|
|
129
|
+
useEffect(() => () => { chainCtrlRef.current?.abort(); }, []);
|
|
130
|
+
|
|
131
|
+
async function loadChain(d: { decision_id: string; category?: string; outcome?: string }) {
|
|
132
|
+
chainCtrlRef.current?.abort();
|
|
133
|
+
const ctrl = new AbortController();
|
|
134
|
+
chainCtrlRef.current = ctrl;
|
|
135
|
+
|
|
136
|
+
setSelected(d);
|
|
137
|
+
setChainLoading(true);
|
|
138
|
+
setChain([]);
|
|
139
|
+
setError("");
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(`/api/decisions/${encodeURIComponent(d.decision_id)}/chain`, { signal: ctrl.signal });
|
|
142
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
if (res.status === 207 && !ctrl.signal.aborted) {
|
|
145
|
+
setError(data.message || "Warning: Partial success loading chain.");
|
|
146
|
+
}
|
|
147
|
+
if (!ctrl.signal.aborted) setChain(data.chain || []);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (e instanceof Error && e.name !== "AbortError") {
|
|
150
|
+
setError(e.message);
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
if (!ctrl.signal.aborted) setChainLoading(false);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const filtered = useMemo(() => {
|
|
158
|
+
const q = filter.toLowerCase().trim();
|
|
159
|
+
if (!q) return decisions;
|
|
160
|
+
return decisions.filter((d) =>
|
|
161
|
+
d.decision_id.toLowerCase().includes(q) ||
|
|
162
|
+
(d.category ?? "").toLowerCase().includes(q) ||
|
|
163
|
+
(d.outcome ?? "").toLowerCase().includes(q)
|
|
164
|
+
);
|
|
165
|
+
}, [decisions, filter]);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="ws-page" style={{ flexDirection: "row" }}>
|
|
169
|
+
{/* ── Sidebar list ── */}
|
|
170
|
+
<div className="ws-sidebar" style={{ width: 290 }}>
|
|
171
|
+
<div className="ws-sidebar-header">
|
|
172
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
|
173
|
+
<div style={{ width: 30, height: 30, borderRadius: 9, background: "var(--ws-accent-soft)", border: "1px solid var(--ws-border-strong)", display: "grid", placeItems: "center", color: "var(--ws-accent)", flexShrink: 0 }}>
|
|
174
|
+
<Scale size={15} />
|
|
175
|
+
</div>
|
|
176
|
+
<div style={{ color: "var(--ws-text)", fontSize: 14, fontWeight: 700 }}>Decisions</div>
|
|
177
|
+
{decisions.length > 0 && !listLoading && (
|
|
178
|
+
<span className="ws-pill ws-pill--mono" style={{ marginLeft: "auto" }}>{decisions.length}</span>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
<div style={{ position: "relative" }}>
|
|
182
|
+
<Search size={12} color="var(--ws-text-dim)" style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", pointerEvents: "none" }} />
|
|
183
|
+
<input
|
|
184
|
+
className="ws-input"
|
|
185
|
+
type="text"
|
|
186
|
+
placeholder="Filter by ID, category, outcome…"
|
|
187
|
+
value={filter}
|
|
188
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
189
|
+
style={{ paddingLeft: 30, fontSize: 12 }}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="ws-sidebar-body">
|
|
195
|
+
{listLoading ? (
|
|
196
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
197
|
+
{[1,2,3,4,5].map((i) => <div key={i} className="ws-skeleton" style={{ height: 58 }} />)}
|
|
198
|
+
</div>
|
|
199
|
+
) : filtered.length === 0 ? (
|
|
200
|
+
<div className="ws-empty" style={{ padding: "28px 12px" }}>
|
|
201
|
+
<div className="ws-empty-title">{decisions.length === 0 ? "No decisions" : "No matches"}</div>
|
|
202
|
+
<div className="ws-empty-body">{decisions.length === 0 ? "No decisions available in the graph." : "Adjust your filter."}</div>
|
|
203
|
+
</div>
|
|
204
|
+
) : (
|
|
205
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
206
|
+
{filtered.map((d) => {
|
|
207
|
+
const active = selected?.decision_id === d.decision_id;
|
|
208
|
+
return (
|
|
209
|
+
<button
|
|
210
|
+
key={d.decision_id}
|
|
211
|
+
className={`ws-list-item${active ? " ws-list-item--active" : ""}`}
|
|
212
|
+
onClick={() => void loadChain(d)}
|
|
213
|
+
>
|
|
214
|
+
<div style={{ fontWeight: 700, fontSize: 12, marginBottom: 5, color: active ? "#ffeef1" : "var(--ws-text)" }}>
|
|
215
|
+
{d.decision_id}
|
|
216
|
+
</div>
|
|
217
|
+
<div style={{ display: "flex", alignItems: "center", gap: 5, flexWrap: "wrap" }}>
|
|
218
|
+
{d.category && <span style={{ fontSize: 10, color: "var(--ws-text-dim)" }}>{d.category}</span>}
|
|
219
|
+
{d.outcome && <OutcomeBadge outcome={d.outcome} />}
|
|
220
|
+
</div>
|
|
221
|
+
</button>
|
|
222
|
+
);
|
|
223
|
+
})}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* ── Detail pane ── */}
|
|
230
|
+
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", position: "relative" }}>
|
|
231
|
+
<div style={{ position: "absolute", inset: 0, background: "radial-gradient(ellipse 60% 40% at 70% 20%, rgba(255,45,75,0.04), transparent 55%)", pointerEvents: "none" }} />
|
|
232
|
+
|
|
233
|
+
{error ? (
|
|
234
|
+
<div style={{ padding: 12, borderRadius: 14, color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)", margin: "16px 16px 0 16px", zIndex: 2, position: "relative" }}>
|
|
235
|
+
{error}
|
|
236
|
+
</div>
|
|
237
|
+
) : null}
|
|
238
|
+
|
|
239
|
+
{selected ? (
|
|
240
|
+
<div className="ws-scroll ws-padded ws-animate-in" style={{ position: "relative", zIndex: 1 }}>
|
|
241
|
+
{/* Decision header */}
|
|
242
|
+
<div style={{ marginBottom: 28 }}>
|
|
243
|
+
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12, flexWrap: "wrap", marginBottom: 10 }}>
|
|
244
|
+
<div>
|
|
245
|
+
<div className="ws-eyebrow" style={{ marginBottom: 6 }}>Decision Record</div>
|
|
246
|
+
<h2 className="ws-title">{selected.decision_id}</h2>
|
|
247
|
+
</div>
|
|
248
|
+
{selected.outcome && <OutcomeBadge outcome={selected.outcome} />}
|
|
249
|
+
</div>
|
|
250
|
+
{selected.category && (
|
|
251
|
+
<span className="ws-pill ws-pill--mono">{selected.category}</span>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Chain section */}
|
|
256
|
+
<div className="ws-card" style={{ padding: 22 }}>
|
|
257
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 20 }}>
|
|
258
|
+
<div style={{ width: 8, height: 8, borderRadius: 999, background: "linear-gradient(135deg, var(--ws-accent), var(--ws-amber))", boxShadow: "0 0 10px rgba(255,45,75,0.4)" }} />
|
|
259
|
+
<div style={{ color: "var(--ws-text)", fontSize: 14, fontWeight: 700 }}>Causal Chain</div>
|
|
260
|
+
{chain.length > 0 && !chainLoading && (
|
|
261
|
+
<span className="ws-pill ws-pill--accent" style={{ marginLeft: "auto" }}>{chain.length} step{chain.length !== 1 ? "s" : ""}</span>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
<CausalFlow chain={chain} loading={chainLoading} />
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
) : (
|
|
268
|
+
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", position: "relative", zIndex: 1 }}>
|
|
269
|
+
<div className="ws-empty">
|
|
270
|
+
<div className="ws-empty-icon"><Scale size={32} /></div>
|
|
271
|
+
<div className="ws-empty-title">No decision selected</div>
|
|
272
|
+
<div className="ws-empty-body">Select a decision from the list to inspect its causal chain and metadata.</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { GitMerge, ArrowRight, CheckCircle2, AlertCircle, Loader2 } from "lucide-react";
|
|
3
|
+
import { logEvent } from "../../store/registryStore";
|
|
4
|
+
|
|
5
|
+
interface FieldRow { label: string; primary: string; duplicate: string; differs: boolean }
|
|
6
|
+
|
|
7
|
+
const MOCK_FIELDS: FieldRow[] = [
|
|
8
|
+
{ label: "Name", primary: "Sample Company Inc.", duplicate: "Sample Company", differs: true },
|
|
9
|
+
{ label: "Founded", primary: "2004-05-12", duplicate: "2004-05-12", differs: false },
|
|
10
|
+
{ label: "Type", primary: "Organization", duplicate: "Organisation", differs: true },
|
|
11
|
+
{ label: "Country", primary: "US", duplicate: "US", differs: false },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function DiffMergeWorkspace() {
|
|
15
|
+
const [primaryId, setPrimaryId] = useState("n-primary-1");
|
|
16
|
+
const [duplicateId, setDuplicateId] = useState("n-dup-2");
|
|
17
|
+
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
|
18
|
+
const [msg, setMsg] = useState("");
|
|
19
|
+
|
|
20
|
+
async function handleMerge() {
|
|
21
|
+
setStatus("loading");
|
|
22
|
+
setMsg("");
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch("/api/enrich/merge", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ primary_id: primaryId, duplicate_ids: [duplicateId] }),
|
|
28
|
+
});
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
if (data.merged_into) {
|
|
31
|
+
setStatus("success");
|
|
32
|
+
setMsg(`Merged → ${data.merged_into} · ${data.edges_updated ?? 0} edges redirected`);
|
|
33
|
+
logEvent("merge", `Merged ${duplicateId} → ${data.merged_into} · ${data.edges_updated ?? 0} edges redirected`, {
|
|
34
|
+
primary: data.merged_into, duplicate: duplicateId, edgesUpdated: data.edges_updated,
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error(data.detail || "Unexpected response");
|
|
38
|
+
}
|
|
39
|
+
} catch (e: unknown) {
|
|
40
|
+
setStatus("error");
|
|
41
|
+
setMsg(e instanceof Error ? e.message : "Merge failed");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="ws-page ws-scroll">
|
|
47
|
+
<div className="ws-padded" style={{ display: "flex", flexDirection: "column", gap: 22, maxWidth: 1000, margin: "0 auto", width: "100%" }}>
|
|
48
|
+
{/* Header */}
|
|
49
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
|
50
|
+
<div style={{ width: 42, height: 42, borderRadius: 13, background: "var(--ws-purple-soft)", border: "1px solid rgba(192,132,252,0.3)", display: "grid", placeItems: "center", color: "var(--ws-purple)", flexShrink: 0 }}>
|
|
51
|
+
<GitMerge size={20} />
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<h2 className="ws-title" style={{ fontSize: 18 }}>Entity Diff & Merge</h2>
|
|
55
|
+
<div className="ws-body" style={{ marginTop: 2 }}>Compare suspected duplicates side-by-side and reconcile them into a single canonical entity.</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* ID inputs */}
|
|
60
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr auto 1fr", gap: 12, alignItems: "end" }}>
|
|
61
|
+
<div>
|
|
62
|
+
<label className="ws-label">Primary Node ID (keep)</label>
|
|
63
|
+
<input className="ws-input" value={primaryId} onChange={(e) => { setPrimaryId(e.target.value); setStatus("idle"); }} placeholder="e.g. n-primary-1" />
|
|
64
|
+
</div>
|
|
65
|
+
<div style={{ paddingBottom: 2, color: "var(--ws-text-dim)" }}>
|
|
66
|
+
<ArrowRight size={18} />
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<label className="ws-label">Duplicate Node ID (remove)</label>
|
|
70
|
+
<input className="ws-input" value={duplicateId} onChange={(e) => { setDuplicateId(e.target.value); setStatus("idle"); }} placeholder="e.g. n-dup-2" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Diff table */}
|
|
75
|
+
<div className="ws-card" style={{ padding: 0, overflow: "hidden" }}>
|
|
76
|
+
<div style={{ padding: "6px 16px", background: "rgba(242,182,109,0.06)", borderBottom: "1px solid rgba(242,182,109,0.15)", display: "flex", alignItems: "center", gap: 6 }}>
|
|
77
|
+
<span style={{ fontSize: 10, fontWeight: 700, color: "var(--ws-amber)", letterSpacing: "0.08em", textTransform: "uppercase" }}>Sample preview</span>
|
|
78
|
+
<span style={{ fontSize: 11, color: "var(--ws-text-dim)" }}>— field comparison will load from the graph once the backend is connected</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div style={{ display: "grid", gridTemplateColumns: "140px 1fr 1fr", background: "rgba(0,0,0,0.28)", borderBottom: "1px solid var(--ws-border)" }}>
|
|
81
|
+
<div style={{ padding: "10px 16px", fontSize: 11, fontWeight: 700, color: "var(--ws-text-dim)", letterSpacing: "0.08em", textTransform: "uppercase" }}>Field</div>
|
|
82
|
+
<div style={{ padding: "10px 16px", fontSize: 11, fontWeight: 700, color: "var(--ws-accent)", letterSpacing: "0.08em", textTransform: "uppercase", borderLeft: "1px solid var(--ws-border)" }}>Primary (keep)</div>
|
|
83
|
+
<div style={{ padding: "10px 16px", fontSize: 11, fontWeight: 700, color: "#fca5a5", letterSpacing: "0.08em", textTransform: "uppercase", borderLeft: "1px solid var(--ws-border)" }}>Duplicate (remove)</div>
|
|
84
|
+
</div>
|
|
85
|
+
{MOCK_FIELDS.map((row) => (
|
|
86
|
+
<div key={row.label} style={{ display: "grid", gridTemplateColumns: "140px 1fr 1fr", borderBottom: "1px solid rgba(255,45,75,0.06)", background: row.differs ? "rgba(242,182,109,0.03)" : "transparent" }}>
|
|
87
|
+
<div style={{ padding: "12px 16px", fontSize: 12, fontWeight: 600, color: "var(--ws-text-dim)" }}>{row.label}</div>
|
|
88
|
+
<div style={{ padding: "12px 16px", fontSize: 13, color: "var(--ws-text)", borderLeft: "1px solid var(--ws-border)" }}>{row.primary}</div>
|
|
89
|
+
<div style={{ padding: "12px 16px", fontSize: 13, color: row.differs ? "#fbbf24" : "var(--ws-text)", fontWeight: row.differs ? 700 : 400, borderLeft: "1px solid var(--ws-border)" }}>
|
|
90
|
+
{row.duplicate}
|
|
91
|
+
{row.differs && <span className="ws-pill ws-pill--amber" style={{ marginLeft: 8, fontSize: 9 }}>diff</span>}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Status & action */}
|
|
98
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
99
|
+
{status === "success" && (
|
|
100
|
+
<div style={{ display: "flex", alignItems: "center", gap: 7, color: "#6ee7b7", fontSize: 13 }}>
|
|
101
|
+
<CheckCircle2 size={15} />{msg}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
{status === "error" && (
|
|
105
|
+
<div style={{ display: "flex", alignItems: "center", gap: 7, color: "#fca5a5", fontSize: 13 }}>
|
|
106
|
+
<AlertCircle size={15} />{msg}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
<button
|
|
110
|
+
className="ws-btn ws-btn--primary"
|
|
111
|
+
onClick={handleMerge}
|
|
112
|
+
disabled={status === "loading" || !primaryId || !duplicateId}
|
|
113
|
+
style={{ marginLeft: "auto" }}
|
|
114
|
+
>
|
|
115
|
+
{status === "loading" ? <><Loader2 size={14} className="ws-spin" />Merging…</> : <><GitMerge size={14} />Confirm Merge</>}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|