@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,177 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import {
9
+ MarkdownClientError,
10
+ applyMarkdownResource,
11
+ readMarkdownResource,
12
+ type MarkdownApplyResult,
13
+ } from "./markdownResourceClient";
14
+ import {
15
+ cancelEdit,
16
+ createEditSession,
17
+ createLoadingSession,
18
+ isDirty,
19
+ saveFailed,
20
+ saveStarted,
21
+ saveSucceeded,
22
+ updateDraft,
23
+ type MarkdownEditorError,
24
+ type MarkdownEditSession,
25
+ type MarkdownResourceRef,
26
+ } from "./markdownEditorState";
27
+
28
+ interface MarkdownEditorOptions {
29
+ resource?: MarkdownResourceRef;
30
+ onApplied?: (result: MarkdownApplyResult) => void;
31
+ onDirtyChange?: (dirty: boolean) => void;
32
+ }
33
+
34
+ interface KeyedError {
35
+ resourceKey: string;
36
+ error: MarkdownEditorError;
37
+ }
38
+
39
+ function keyOf(resource?: MarkdownResourceRef): string {
40
+ return resource ? `${resource.kind}:${resource.id}` : "";
41
+ }
42
+
43
+ function normalizeError(failure: unknown): MarkdownEditorError {
44
+ if (failure instanceof MarkdownClientError) return failure;
45
+ return {
46
+ kind: "network",
47
+ message: "The Markdown service could not be reached. Your draft was kept.",
48
+ };
49
+ }
50
+
51
+ export function useMarkdownEditor({
52
+ resource,
53
+ onApplied,
54
+ onDirtyChange,
55
+ }: MarkdownEditorOptions) {
56
+ const resourceKey = keyOf(resource);
57
+ const [session, setSession] = useState<MarkdownEditSession | null>(null);
58
+ const [viewError, setViewError] = useState<KeyedError | null>(null);
59
+ const [renderedResourceKey, setRenderedResourceKey] = useState(resourceKey);
60
+ const loadGenerationRef = useRef(0);
61
+
62
+ if (renderedResourceKey !== resourceKey) {
63
+ setRenderedResourceKey(resourceKey);
64
+ setSession(null);
65
+ setViewError(null);
66
+ }
67
+
68
+ useLayoutEffect(() => {
69
+ loadGenerationRef.current += 1;
70
+ }, [resourceKey]);
71
+
72
+ const activeSession = session && keyOf(session.resource) === resourceKey
73
+ ? session
74
+ : null;
75
+ const dirty = isDirty(activeSession);
76
+ const editing = activeSession !== null
77
+ && activeSession.status !== "viewing"
78
+ && activeSession.status !== "loading-document";
79
+ const saving = activeSession?.status === "saving";
80
+ const loading = activeSession?.status === "loading-document";
81
+ const error = activeSession?.error
82
+ ?? (viewError?.resourceKey === resourceKey ? viewError.error : null);
83
+
84
+ useEffect(() => {
85
+ onDirtyChange?.(dirty);
86
+ return () => {
87
+ if (dirty) onDirtyChange?.(false);
88
+ };
89
+ }, [dirty, onDirtyChange]);
90
+
91
+ useEffect(() => {
92
+ if (!dirty) return;
93
+ const protectDraft = (event: BeforeUnloadEvent) => {
94
+ event.preventDefault();
95
+ event.returnValue = "";
96
+ };
97
+ window.addEventListener("beforeunload", protectDraft);
98
+ return () => window.removeEventListener("beforeunload", protectDraft);
99
+ }, [dirty]);
100
+
101
+ const beginEdit = useCallback(async () => {
102
+ if (!resource) return false;
103
+ const loadGeneration = ++loadGenerationRef.current;
104
+ setViewError(null);
105
+ setSession(createLoadingSession(resource));
106
+ try {
107
+ const document = await readMarkdownResource(resource);
108
+ if (loadGeneration !== loadGenerationRef.current) return false;
109
+ setSession(createEditSession(resource, document));
110
+ return true;
111
+ } catch (failure) {
112
+ if (loadGeneration !== loadGenerationRef.current) return false;
113
+ setSession(null);
114
+ setViewError({ resourceKey, error: normalizeError(failure) });
115
+ return false;
116
+ }
117
+ }, [resource, resourceKey]);
118
+
119
+ const discard = useCallback(() => {
120
+ if (!activeSession || saving) return;
121
+ setSession(cancelEdit());
122
+ setViewError(null);
123
+ }, [activeSession, saving]);
124
+
125
+ const save = useCallback(async () => {
126
+ if (!activeSession || saving || !dirty) return false;
127
+ const resourceGeneration = loadGenerationRef.current;
128
+ const pending = saveStarted(activeSession);
129
+ setSession(pending);
130
+ try {
131
+ const result = await applyMarkdownResource(
132
+ pending.resource,
133
+ pending.draft,
134
+ pending.baseRevision,
135
+ );
136
+ if (resourceGeneration !== loadGenerationRef.current) return false;
137
+ setSession(saveSucceeded(pending, result));
138
+ onApplied?.(result);
139
+ return true;
140
+ } catch (failure) {
141
+ if (resourceGeneration !== loadGenerationRef.current) return false;
142
+ setSession(saveFailed(pending, normalizeError(failure)));
143
+ return false;
144
+ }
145
+ }, [activeSession, dirty, onApplied, saving]);
146
+
147
+ const reloadLatest = useCallback(async () => {
148
+ if (!resource || saving) return;
149
+ if (
150
+ dirty
151
+ && !window.confirm("Discard this draft and reload the latest applied version?")
152
+ ) return;
153
+ await beginEdit();
154
+ }, [beginEdit, dirty, resource, saving]);
155
+
156
+ const changeDraft = useCallback((draft: string) => {
157
+ setSession((current) => (
158
+ current && keyOf(current.resource) === resourceKey
159
+ ? updateDraft(current, draft)
160
+ : current
161
+ ));
162
+ }, [resourceKey]);
163
+
164
+ return {
165
+ session: activeSession,
166
+ error,
167
+ dirty,
168
+ editing,
169
+ saving,
170
+ loading,
171
+ beginEdit,
172
+ discard,
173
+ save,
174
+ reloadLatest,
175
+ changeDraft,
176
+ };
177
+ }
@@ -0,0 +1,196 @@
1
+ import { useState, useCallback } from "react";
2
+ import { useDropzone } from "react-dropzone";
3
+ import { UploadCloud, Download, FileJson, FileText, CheckCircle2, AlertCircle, Loader2 } from "lucide-react";
4
+ import { logEvent } from "../../store/registryStore";
5
+
6
+ interface Toast { id: number; type: "success" | "error"; text: string }
7
+
8
+ export function ImportExportWorkspace() {
9
+ const [file, setFile] = useState<File | null>(null);
10
+ const [isUploading, setIsUploading] = useState(false);
11
+ const [exportFormat, setExportFormat] = useState<"json" | "csv">("json");
12
+ const [isExporting, setIsExporting] = useState(false);
13
+ const [toasts, setToasts] = useState<Toast[]>([]);
14
+
15
+ function showToast(type: Toast["type"], text: string) {
16
+ const id = Date.now();
17
+ setToasts((p) => [...p, { id, type, text }]);
18
+ setTimeout(() => setToasts((p) => p.filter((t) => t.id !== id)), 5000);
19
+ }
20
+
21
+ const onDrop = useCallback((accepted: File[]) => {
22
+ if (accepted.length > 0) setFile(accepted[0]);
23
+ }, []);
24
+
25
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
26
+ onDrop,
27
+ accept: { "application/json": [".json"], "text/csv": [".csv"] },
28
+ maxFiles: 1,
29
+ });
30
+
31
+ async function handleImport() {
32
+ if (!file) return;
33
+ setIsUploading(true);
34
+ try {
35
+ const fd = new FormData();
36
+ fd.append("file", file);
37
+ const res = await fetch("/api/import", { method: "POST", body: fd });
38
+ if (!res.ok) { const e = await res.json(); throw new Error(e.detail || "Import failed"); }
39
+ const data = await res.json();
40
+ showToast("success", `Imported ${data.nodes_imported} nodes · ${data.edges_imported} edges`);
41
+ logEvent("import", `Imported ${data.nodes_imported} nodes · ${data.edges_imported} edges from ${file.name}`, { file: file.name });
42
+ setFile(null);
43
+ } catch (e: unknown) {
44
+ showToast("error", e instanceof Error ? e.message : "Import failed");
45
+ } finally { setIsUploading(false); }
46
+ }
47
+
48
+ async function handleExport() {
49
+ setIsExporting(true);
50
+ try {
51
+ const res = await fetch("/api/export", {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({ format: exportFormat }),
55
+ });
56
+ if (!res.ok) { const e = await res.json(); throw new Error(e.detail || "Export failed"); }
57
+ const blob = await res.blob();
58
+ const url = URL.createObjectURL(blob);
59
+ const a = document.createElement("a");
60
+ a.href = url;
61
+ a.download = `alphasemantichub_export.${exportFormat}`;
62
+ document.body.appendChild(a);
63
+ a.click();
64
+ document.body.removeChild(a);
65
+ URL.revokeObjectURL(url);
66
+ showToast("success", `Export ready — alphasemantichub_export.${exportFormat}`);
67
+ logEvent("export", `Exported graph as ${exportFormat.toUpperCase()}`, { format: exportFormat });
68
+ } catch (e: unknown) {
69
+ showToast("error", e instanceof Error ? e.message : "Export failed");
70
+ } finally { setIsExporting(false); }
71
+ }
72
+
73
+ return (
74
+ <div className="ws-page ws-scroll">
75
+ <div className="ws-padded" style={{ display: "flex", flexDirection: "column", gap: 24, maxWidth: 980, margin: "0 auto", width: "100%" }}>
76
+ {/* Page header */}
77
+ <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
78
+ <div style={{ width: 42, height: 42, borderRadius: 13, background: "var(--ws-accent-soft)", border: "1px solid var(--ws-border-strong)", display: "grid", placeItems: "center", color: "var(--ws-accent)", flexShrink: 0 }}>
79
+ <UploadCloud size={20} />
80
+ </div>
81
+ <div>
82
+ <h2 className="ws-title" style={{ fontSize: 18 }}>Import &amp; Export</h2>
83
+ <div className="ws-body" style={{ marginTop: 2 }}>Ingest new graph datasets or extract the current knowledge base.</div>
84
+ </div>
85
+ </div>
86
+
87
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 18 }}>
88
+ {/* ── Import card ── */}
89
+ <div className="ws-card" style={{ padding: 24, display: "flex", flexDirection: "column", gap: 18 }}>
90
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
91
+ <UploadCloud size={16} color="var(--ws-accent)" />
92
+ <div style={{ color: "var(--ws-text)", fontWeight: 700, fontSize: 14 }}>Import Entities &amp; Relations</div>
93
+ </div>
94
+
95
+ {/* Dropzone */}
96
+ <div
97
+ {...getRootProps()}
98
+ style={{
99
+ border: `2px dashed ${isDragActive ? "var(--ws-accent)" : "var(--ws-border)"}`,
100
+ borderRadius: "var(--ws-radius)",
101
+ background: isDragActive ? "var(--ws-accent-soft)" : "rgba(0,0,0,0.18)",
102
+ padding: 32,
103
+ textAlign: "center",
104
+ cursor: "pointer",
105
+ transition: "all 200ms ease",
106
+ display: "flex",
107
+ flexDirection: "column",
108
+ alignItems: "center",
109
+ gap: 10,
110
+ }}
111
+ >
112
+ <input {...getInputProps()} />
113
+ {file ? (
114
+ <>
115
+ {file.name.endsWith(".json") ? <FileJson size={40} color="#4cc38a" /> : <FileText size={40} color="#4cc38a" />}
116
+ <div style={{ color: "var(--ws-text)", fontWeight: 700 }}>{file.name}</div>
117
+ <div className="ws-body" style={{ fontSize: 11 }}>{(file.size / 1024).toFixed(1)} KB — click to replace</div>
118
+ </>
119
+ ) : (
120
+ <>
121
+ <UploadCloud size={36} color="var(--ws-accent)" style={{ opacity: 0.7 }} />
122
+ <div style={{ color: "var(--ws-text)", fontWeight: 600 }}>Drag &amp; drop or click to browse</div>
123
+ <div className="ws-pill ws-pill--mono">.json</div>
124
+ <span style={{ color: "var(--ws-text-dim)", fontSize: 11 }}>or</span>
125
+ <div className="ws-pill ws-pill--mono">.csv</div>
126
+ </>
127
+ )}
128
+ </div>
129
+
130
+ <button
131
+ className="ws-btn ws-btn--primary"
132
+ onClick={handleImport}
133
+ disabled={!file || isUploading}
134
+ style={{ width: "100%", justifyContent: "center" }}
135
+ >
136
+ {isUploading ? <><Loader2 size={15} className="ws-spin" />Uploading…</> : <><UploadCloud size={15} />Upload to Graph</>}
137
+ </button>
138
+ </div>
139
+
140
+ {/* ── Export card ── */}
141
+ <div className="ws-card" style={{ padding: 24, display: "flex", flexDirection: "column", gap: 18 }}>
142
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
143
+ <Download size={16} color="var(--ws-purple)" />
144
+ <div style={{ color: "var(--ws-text)", fontWeight: 700, fontSize: 14 }}>Export Graph Snapshot</div>
145
+ </div>
146
+
147
+ <div>
148
+ <label className="ws-label">Format</label>
149
+ <div style={{ display: "flex", gap: 8 }}>
150
+ {(["json", "csv"] as const).map((fmt) => (
151
+ <button
152
+ key={fmt}
153
+ className={`ws-btn ${exportFormat === fmt ? "ws-btn--primary" : "ws-btn--ghost"}`}
154
+ style={{ flex: 1, justifyContent: "center", textTransform: "uppercase", fontSize: 12 }}
155
+ onClick={() => setExportFormat(fmt)}
156
+ >
157
+ {fmt === "json" ? <FileJson size={14} /> : <FileText size={14} />}
158
+ {fmt}
159
+ </button>
160
+ ))}
161
+ </div>
162
+ </div>
163
+
164
+ <div style={{ flex: 1, padding: "14px 16px", borderRadius: "var(--ws-radius-sm)", background: "rgba(0,0,0,0.22)", border: "1px solid var(--ws-border)" }}>
165
+ <div style={{ color: "var(--ws-text-muted)", fontWeight: 700, fontSize: 12, marginBottom: 6 }}>What's included</div>
166
+ <div className="ws-body" style={{ fontSize: 12 }}>
167
+ {exportFormat === "json"
168
+ ? "Full graph snapshot: all node properties, edge weights, entity metadata and semantic groups in a standardized JSON payload."
169
+ : "Flattened CSV: nodes and edges as rows. Complex nested properties are stringified. Best for spreadsheet analysis."}
170
+ </div>
171
+ </div>
172
+
173
+ <button
174
+ className="ws-btn"
175
+ onClick={handleExport}
176
+ disabled={isExporting}
177
+ style={{ width: "100%", justifyContent: "center", background: "var(--ws-purple-soft)", borderColor: "rgba(192,132,252,0.3)", color: "#d8b4fe" }}
178
+ >
179
+ {isExporting ? <><Loader2 size={15} className="ws-spin" />Preparing…</> : <><Download size={15} />Download Export</>}
180
+ </button>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ {/* Toasts */}
186
+ <div style={{ position: "fixed", bottom: 28, right: 28, display: "flex", flexDirection: "column", gap: 10, zIndex: 1000 }}>
187
+ {toasts.map((t) => (
188
+ <div key={t.id} className="ws-animate-in" style={{ display: "flex", alignItems: "center", gap: 10, padding: "13px 18px", borderRadius: "var(--ws-radius-sm)", background: t.type === "success" ? "rgba(16,36,22,0.96)" : "rgba(40,10,10,0.96)", border: `1px solid ${t.type === "success" ? "rgba(76,195,138,0.4)" : "rgba(255,123,114,0.4)"}`, boxShadow: "0 8px 24px rgba(0,0,0,0.5)", backdropFilter: "blur(12px)" }}>
189
+ {t.type === "success" ? <CheckCircle2 size={16} color="#4cc38a" /> : <AlertCircle size={16} color="#ff7b72" />}
190
+ <span style={{ color: "var(--ws-text)", fontSize: 13, fontWeight: 500 }}>{t.text}</span>
191
+ </div>
192
+ ))}
193
+ </div>
194
+ </div>
195
+ );
196
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * src/workspaces/LineageWorkspace/LineageDiagram.tsx
3
+ */
4
+ import { useEffect, useState } from "react";
5
+ import { Link2 } from "lucide-react";
6
+ import { ReactFlow, Background, Controls } from "@xyflow/react";
7
+ import "@xyflow/react/dist/style.css";
8
+
9
+ const THEME_CSS = `
10
+ .react-flow { background: var(--ws-bg, #080407); }
11
+ .react-flow__node-group {
12
+ background: rgba(255,45,75,0.04);
13
+ border: 1px dashed rgba(255,45,75,0.18);
14
+ border-radius: 10px;
15
+ }
16
+ .react-flow__node-default {
17
+ background: rgba(6,13,26,0.92);
18
+ color: var(--ws-text, #ffe9ec);
19
+ border: 1px solid rgba(255,45,75,0.22);
20
+ border-radius: 8px;
21
+ padding: 10px 12px;
22
+ white-space: pre-wrap;
23
+ font-size: 12px;
24
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
25
+ }
26
+ .react-flow__controls { background: rgba(6,13,26,0.9); border: 1px solid rgba(255,45,75,0.18); border-radius: 10px; }
27
+ .react-flow__controls-button { background: transparent; border-color: rgba(255,45,75,0.15); color: var(--ws-text-muted, #8f6d75); }
28
+ .react-flow__controls-button:hover { background: rgba(255,45,75,0.1); color: var(--ws-text, #ffe9ec); }
29
+ `;
30
+
31
+ export function LineageDiagram() {
32
+ const [nodes, setNodes] = useState<any[]>([]);
33
+ const [edges, setEdges] = useState<any[]>([]);
34
+ const [searchId, setSearchId] = useState("");
35
+ const [activeId, setActiveId] = useState("");
36
+ const [error, setError] = useState("");
37
+
38
+ const downloadReport = async (format: "json" | "markdown") => {
39
+ if (!activeId) return;
40
+ const response = await fetch(`/api/provenance/report?node_id=${encodeURIComponent(activeId)}&format=${format}`);
41
+ if (!response.ok) {
42
+ return;
43
+ }
44
+ const blob = await response.blob();
45
+ const url = window.URL.createObjectURL(blob);
46
+ const anchor = document.createElement("a");
47
+ anchor.href = url;
48
+ anchor.download = `${activeId}_provenance.${format === "markdown" ? "md" : "json"}`;
49
+ document.body.appendChild(anchor);
50
+ anchor.click();
51
+ window.URL.revokeObjectURL(url);
52
+ document.body.removeChild(anchor);
53
+ };
54
+
55
+ const [prevActiveId, setPrevActiveId] = useState(activeId);
56
+ if (activeId !== prevActiveId) {
57
+ setPrevActiveId(activeId);
58
+ setError("");
59
+ setNodes([]);
60
+ setEdges([]);
61
+ }
62
+
63
+ useEffect(() => {
64
+ let ignore = false;
65
+ if (!activeId) return;
66
+
67
+ const xLanes = [
68
+ { id: "group_agent", type: "group", position: { x: 50, y: 50 }, style: { width: 800, height: 120 } },
69
+ { id: "group_activity", type: "group", position: { x: 50, y: 200 }, style: { width: 800, height: 120 } },
70
+ { id: "group_entity", type: "group", position: { x: 50, y: 350 }, style: { width: 800, height: 120 } }
71
+ ];
72
+
73
+ const fetchLineage = async () => {
74
+ setError("");
75
+ try {
76
+ const res = await fetch("/api/provenance?node_id=" + encodeURIComponent(activeId));
77
+
78
+ if (!res.ok) {
79
+ const text = await res.text();
80
+ throw new Error(`HTTP ${res.status}: API Route missing or failed. ${text.substring(0, 100)}`);
81
+ }
82
+
83
+ const contentType = res.headers.get("content-type");
84
+ if (!contentType || !contentType.includes("application/json")) {
85
+ throw new Error("Backend returned non-JSON response (likely an HTML fallback).");
86
+ }
87
+
88
+ const data = await res.json();
89
+ if (res.status === 207) {
90
+ setError(data.message || "Warning: Partial success loading lineage.");
91
+ }
92
+
93
+ const counters: Record<string, number> = { "group_agent": 0, "group_activity": 0, "group_entity": 0 };
94
+
95
+ const mappedNodes = data.nodes.map((n: any) => {
96
+ const c = counters[n.parent_id] || 0;
97
+ counters[n.parent_id] = c + 1;
98
+ return {
99
+ id: n.id,
100
+ data: { label: n.label + "\n(" + n.prov_type + ")" },
101
+ position: { x: 50 + c * 180, y: 30 },
102
+ parentId: n.parent_id,
103
+ extent: "parent",
104
+ type: "default"
105
+ };
106
+ });
107
+
108
+ const mappedEdges = data.edges.map((e: any) => ({
109
+ id: e.id,
110
+ source: e.source,
111
+ target: e.target,
112
+ label: e.label,
113
+ animated: true,
114
+ style: { stroke: "#ff2d4b" }
115
+ }));
116
+
117
+ if (!ignore) {
118
+ setNodes([...xLanes, ...mappedNodes]);
119
+ setEdges(mappedEdges);
120
+ }
121
+ } catch (err) {
122
+ setError(err instanceof Error ? err.message : "Failed to load lineage.");
123
+ }
124
+ };
125
+ void fetchLineage();
126
+ return () => { ignore = true; };
127
+ }, [activeId]);
128
+
129
+ return (
130
+ <div style={{ width: "100%", height: "100%", position: "relative", background: "var(--ws-bg, #080407)" }}>
131
+ <style>{THEME_CSS}</style>
132
+
133
+ {/* Toolbar */}
134
+ <div style={{ position: "absolute", top: 14, left: 14, right: 14, zIndex: 10, display: "flex", gap: 8, alignItems: "center", background: "rgba(4,10,18,0.88)", backdropFilter: "blur(14px)", padding: "8px 12px", borderRadius: 12, border: "1px solid rgba(255,45,75,0.16)", boxShadow: "0 4px 20px rgba(0,0,0,0.4)" }}>
135
+ <span className="ws-eyebrow" style={{ color: "var(--ws-accent, #ff2d4b)", marginRight: 4 }}>PROV-O Lineage</span>
136
+ <input
137
+ className="ws-input"
138
+ type="text"
139
+ placeholder="Enter Node ID…"
140
+ value={searchId}
141
+ onChange={(e) => setSearchId(e.target.value)}
142
+ onKeyDown={(e) => { if (e.key === "Enter") setActiveId(searchId); }}
143
+ style={{ width: 200, padding: "6px 10px", fontSize: 12 }}
144
+ />
145
+ <button className="ws-btn ws-btn--primary" style={{ padding: "6px 12px" }} onClick={() => setActiveId(searchId)}>
146
+ Trace
147
+ </button>
148
+ <div style={{ flex: 1 }} />
149
+ <button className="ws-btn ws-btn--ghost" style={{ padding: "6px 12px", fontSize: 11 }} disabled={!activeId} onClick={() => void downloadReport("json")}>
150
+ Export JSON
151
+ </button>
152
+ <button className="ws-btn ws-btn--ghost" style={{ padding: "6px 12px", fontSize: 11 }} disabled={!activeId} onClick={() => void downloadReport("markdown")}>
153
+ Export MD
154
+ </button>
155
+ </div>
156
+
157
+ {error ? (
158
+ <div style={{ position: "absolute", top: 60, left: 14, right: 14, zIndex: 10, padding: 12, borderRadius: 14, color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)" }}>
159
+ {error}
160
+ </div>
161
+ ) : null}
162
+
163
+ {activeId ? (
164
+ <ReactFlow nodes={nodes} edges={edges} fitView>
165
+ <Background color="rgba(255,45,75,0.08)" gap={24} />
166
+ <Controls />
167
+ </ReactFlow>
168
+ ) : (
169
+ <div className="ws-empty" style={{ height: "100%", paddingTop: 72 }}>
170
+ <div className="ws-empty-icon"><Link2 size={36} color="var(--ws-accent)" /></div>
171
+ <div className="ws-empty-title">PROV-O Lineage Viewer</div>
172
+ <div className="ws-empty-body">Enter a Node ID in the toolbar above and click Trace to view its W3C PROV-O lineage diagram.</div>
173
+ </div>
174
+ )}
175
+ </div>
176
+ );
177
+ }