@trustgraph/react-state 1.5.2 → 1.5.4
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/dist/index.cjs +1425 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1311 -21
- package/dist/index.esm.js.map +1 -1
- package/dist/model/message.d.ts +1 -0
- package/dist/model/message.d.ts.map +1 -1
- package/dist/model/settings-types.d.ts +1 -0
- package/dist/model/settings-types.d.ts.map +1 -1
- package/dist/state/chat-session.d.ts.map +1 -1
- package/dist/state/chunked-download.d.ts +56 -0
- package/dist/state/chunked-download.d.ts.map +1 -0
- package/dist/state/chunked-upload.d.ts +84 -0
- package/dist/state/chunked-upload.d.ts.map +1 -0
- package/dist/state/conversation.d.ts +2 -2
- package/dist/state/conversation.d.ts.map +1 -1
- package/dist/state/document-metadata.d.ts +37 -0
- package/dist/state/document-metadata.d.ts.map +1 -0
- package/dist/state/explainability-store.d.ts +21 -0
- package/dist/state/explainability-store.d.ts.map +1 -0
- package/dist/state/explainability.d.ts +43 -0
- package/dist/state/explainability.d.ts.map +1 -0
- package/dist/state/inference.d.ts +3 -1
- package/dist/state/inference.d.ts.map +1 -1
- package/dist/state/provenance.d.ts +28 -0
- package/dist/state/provenance.d.ts.map +1 -0
- package/dist/utils/explainability.d.ts +120 -0
- package/dist/utils/explainability.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var reactProvider = require('@trustgraph/react-provider');
|
|
4
|
+
var client = require('@trustgraph/client');
|
|
4
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
6
|
var react = require('react');
|
|
6
7
|
var zustand = require('zustand');
|
|
@@ -156,23 +157,25 @@ const useConversation = zustand.create()((set) => ({
|
|
|
156
157
|
setMessages: (v) => set(() => ({
|
|
157
158
|
messages: v,
|
|
158
159
|
})),
|
|
159
|
-
addMessage: (role, text, type) => set((state) => ({
|
|
160
|
+
addMessage: (role, text, type, explainSessionId) => set((state) => ({
|
|
160
161
|
messages: [
|
|
161
162
|
...state.messages,
|
|
162
163
|
{
|
|
163
164
|
role: role,
|
|
164
165
|
text: text,
|
|
165
166
|
type: type || "normal",
|
|
167
|
+
explainSessionId,
|
|
166
168
|
},
|
|
167
169
|
],
|
|
168
170
|
})),
|
|
169
|
-
updateLastMessage: (text) => set((state) => {
|
|
171
|
+
updateLastMessage: (text, explainSessionId) => set((state) => {
|
|
170
172
|
if (state.messages.length === 0)
|
|
171
173
|
return state;
|
|
172
174
|
const messages = [...state.messages];
|
|
173
175
|
messages[messages.length - 1] = {
|
|
174
176
|
...messages[messages.length - 1],
|
|
175
177
|
text: text,
|
|
178
|
+
...(explainSessionId !== undefined && { explainSessionId }),
|
|
176
179
|
};
|
|
177
180
|
return { messages };
|
|
178
181
|
}),
|
|
@@ -312,6 +315,7 @@ const DEFAULT_SETTINGS = {
|
|
|
312
315
|
flowBlueprintEditor: false, // Off by default - experimental feature
|
|
313
316
|
structuredQuery: false, // Off by default
|
|
314
317
|
llmModels: false, // Off by default
|
|
318
|
+
explainability: true, // On by default
|
|
315
319
|
},
|
|
316
320
|
};
|
|
317
321
|
const SETTINGS_STORAGE_KEY = "trustgraph-settings";
|
|
@@ -968,7 +972,7 @@ const useTriples = ({ flow, s, p, o, limit, collection }) => {
|
|
|
968
972
|
};
|
|
969
973
|
|
|
970
974
|
// Helper to get the string value from a Term (IRI or Literal)
|
|
971
|
-
const getTermValue$
|
|
975
|
+
const getTermValue$3 = (term) => {
|
|
972
976
|
if (term.t === "i")
|
|
973
977
|
return term.i;
|
|
974
978
|
if (term.t === "l")
|
|
@@ -1087,7 +1091,7 @@ const queryLabel = (socket, uri, add, remove, collection) => {
|
|
|
1087
1091
|
// If got a result, return the label, otherwise the URI
|
|
1088
1092
|
// can be its own label
|
|
1089
1093
|
if (triples.length > 0)
|
|
1090
|
-
return getTermValue$
|
|
1094
|
+
return getTermValue$3(triples[0].o);
|
|
1091
1095
|
else
|
|
1092
1096
|
return uri;
|
|
1093
1097
|
})
|
|
@@ -1104,7 +1108,7 @@ const queryLabel = (socket, uri, add, remove, collection) => {
|
|
|
1104
1108
|
// Returns a promise
|
|
1105
1109
|
const labelS = (socket, triples, add, remove, collection) => {
|
|
1106
1110
|
return Promise.all(triples.map((t) => {
|
|
1107
|
-
return queryLabel(socket, getTermValue$
|
|
1111
|
+
return queryLabel(socket, getTermValue$3(t.s), add, remove, collection).then((label) => {
|
|
1108
1112
|
return {
|
|
1109
1113
|
...t,
|
|
1110
1114
|
s: {
|
|
@@ -1119,7 +1123,7 @@ const labelS = (socket, triples, add, remove, collection) => {
|
|
|
1119
1123
|
// Returns a promise
|
|
1120
1124
|
const labelP = (socket, triples, add, remove, collection) => {
|
|
1121
1125
|
return Promise.all(triples.map((t) => {
|
|
1122
|
-
return queryLabel(socket, getTermValue$
|
|
1126
|
+
return queryLabel(socket, getTermValue$3(t.p), add, remove, collection).then((label) => {
|
|
1123
1127
|
return {
|
|
1124
1128
|
...t,
|
|
1125
1129
|
p: {
|
|
@@ -1152,7 +1156,7 @@ const labelO = (socket, triples, add, remove, collection) => {
|
|
|
1152
1156
|
...t,
|
|
1153
1157
|
o: {
|
|
1154
1158
|
...t.o,
|
|
1155
|
-
label: getTermValue$
|
|
1159
|
+
label: getTermValue$3(t.o),
|
|
1156
1160
|
},
|
|
1157
1161
|
});
|
|
1158
1162
|
});
|
|
@@ -1188,7 +1192,7 @@ const getTriples = (socket, uri, add, remove, limit, collection) => {
|
|
|
1188
1192
|
// Functionality here helps construct subgraphs for react-force-graph
|
|
1189
1193
|
// visualisation
|
|
1190
1194
|
// Helper to get the string value from a Term (IRI or Literal)
|
|
1191
|
-
const getTermValue$
|
|
1195
|
+
const getTermValue$2 = (term) => {
|
|
1192
1196
|
if (term.t === "i")
|
|
1193
1197
|
return term.i;
|
|
1194
1198
|
if (term.t === "l")
|
|
@@ -1216,11 +1220,11 @@ const updateSubgraphTriples = (sg, triples) => {
|
|
|
1216
1220
|
continue;
|
|
1217
1221
|
}
|
|
1218
1222
|
// Source has a URI, that can be its unique ID
|
|
1219
|
-
const sourceId = getTermValue$
|
|
1223
|
+
const sourceId = getTermValue$2(t.s);
|
|
1220
1224
|
// Target is always an entity now (we filtered out literals above)
|
|
1221
|
-
const targetId = getTermValue$
|
|
1225
|
+
const targetId = getTermValue$2(t.o);
|
|
1222
1226
|
// Links have an ID so that this edge is unique
|
|
1223
|
-
const linkId = getTermValue$
|
|
1227
|
+
const linkId = getTermValue$2(t.s) + "@@" + getTermValue$2(t.p) + "@@" + getTermValue$2(t.o);
|
|
1224
1228
|
if (!nodeIds.has(sourceId)) {
|
|
1225
1229
|
const sLabeled = t.s;
|
|
1226
1230
|
const n = {
|
|
@@ -1731,10 +1735,14 @@ const useInference = ({ flow } = {}) => {
|
|
|
1731
1735
|
// Use explicit param if provided, otherwise fall back to session state
|
|
1732
1736
|
const effectiveFlow = flow ?? sessionFlowId;
|
|
1733
1737
|
/**
|
|
1734
|
-
* Graph RAG inference with entity discovery
|
|
1738
|
+
* Graph RAG inference with entity discovery and optional explainability
|
|
1739
|
+
* Explainability events are only tracked if callbacks.onExplain is provided
|
|
1735
1740
|
*/
|
|
1736
1741
|
const graphRagMutation = reactQuery.useMutation({
|
|
1737
1742
|
mutationFn: async ({ input, options, collection, callbacks, }) => {
|
|
1743
|
+
// Only collect explain events if caller provided onExplain callback
|
|
1744
|
+
const wantsExplainability = !!callbacks?.onExplain;
|
|
1745
|
+
const explainEvents = wantsExplainability ? [] : undefined;
|
|
1738
1746
|
// If callbacks provided, use streaming API
|
|
1739
1747
|
const response = callbacks
|
|
1740
1748
|
? await new Promise((resolve, reject) => {
|
|
@@ -1750,9 +1758,16 @@ const useInference = ({ flow } = {}) => {
|
|
|
1750
1758
|
callbacks?.onError?.(error);
|
|
1751
1759
|
reject(new Error(error));
|
|
1752
1760
|
};
|
|
1761
|
+
// Only wire up onExplain if caller wants explainability
|
|
1762
|
+
const onExplain = wantsExplainability
|
|
1763
|
+
? (event) => {
|
|
1764
|
+
explainEvents.push(event);
|
|
1765
|
+
callbacks.onExplain(event);
|
|
1766
|
+
}
|
|
1767
|
+
: undefined;
|
|
1753
1768
|
socket
|
|
1754
1769
|
.flow(effectiveFlow)
|
|
1755
|
-
.graphRagStreaming(input, onChunk, onError, options, collection);
|
|
1770
|
+
.graphRagStreaming(input, onChunk, onError, options, collection, onExplain);
|
|
1756
1771
|
})
|
|
1757
1772
|
: await socket.flow(effectiveFlow).graphRag(input, options || {}, collection);
|
|
1758
1773
|
// Get embeddings for entity discovery
|
|
@@ -1762,7 +1777,7 @@ const useInference = ({ flow } = {}) => {
|
|
|
1762
1777
|
const entities = await socket
|
|
1763
1778
|
.flow(effectiveFlow)
|
|
1764
1779
|
.graphEmbeddingsQuery(embeddings, options?.entityLimit || 10, collection);
|
|
1765
|
-
return { response, entities };
|
|
1780
|
+
return { response, entities, explainEvents };
|
|
1766
1781
|
},
|
|
1767
1782
|
});
|
|
1768
1783
|
/**
|
|
@@ -1832,6 +1847,620 @@ const useInference = ({ flow } = {}) => {
|
|
|
1832
1847
|
};
|
|
1833
1848
|
};
|
|
1834
1849
|
|
|
1850
|
+
/**
|
|
1851
|
+
* Zustand store for managing explainability sessions
|
|
1852
|
+
* Sessions are keyed by ID and linked to messages via explainSessionId
|
|
1853
|
+
*/
|
|
1854
|
+
const useExplainabilityStore = zustand.create()((set, get) => ({
|
|
1855
|
+
sessions: {},
|
|
1856
|
+
addSession: (id, session) => set((state) => ({
|
|
1857
|
+
sessions: {
|
|
1858
|
+
...state.sessions,
|
|
1859
|
+
[id]: session,
|
|
1860
|
+
},
|
|
1861
|
+
})),
|
|
1862
|
+
updateSession: (id, partial) => set((state) => ({
|
|
1863
|
+
sessions: {
|
|
1864
|
+
...state.sessions,
|
|
1865
|
+
[id]: {
|
|
1866
|
+
...state.sessions[id],
|
|
1867
|
+
...partial,
|
|
1868
|
+
},
|
|
1869
|
+
},
|
|
1870
|
+
})),
|
|
1871
|
+
getSession: (id) => get().sessions[id],
|
|
1872
|
+
removeSession: (id) => set((state) => {
|
|
1873
|
+
const { [id]: _, ...rest } = state.sessions;
|
|
1874
|
+
return { sessions: rest };
|
|
1875
|
+
}),
|
|
1876
|
+
clearSessions: () => set({ sessions: {} }),
|
|
1877
|
+
}));
|
|
1878
|
+
|
|
1879
|
+
/**
|
|
1880
|
+
* Explainability utilities for parsing and structuring explain events
|
|
1881
|
+
*/
|
|
1882
|
+
/**
|
|
1883
|
+
* Extract event type from explainId URI
|
|
1884
|
+
* e.g., "urn:trustgraph:question:abc123" → "question"
|
|
1885
|
+
*/
|
|
1886
|
+
function getEventType(explainId) {
|
|
1887
|
+
if (explainId.includes("question"))
|
|
1888
|
+
return "question";
|
|
1889
|
+
if (explainId.includes("exploration"))
|
|
1890
|
+
return "exploration";
|
|
1891
|
+
if (explainId.includes("focus"))
|
|
1892
|
+
return "focus";
|
|
1893
|
+
if (explainId.includes("synthesis"))
|
|
1894
|
+
return "synthesis";
|
|
1895
|
+
return "unknown";
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Get term value from a Term object
|
|
1899
|
+
*/
|
|
1900
|
+
function getTermValue$1(term) {
|
|
1901
|
+
if (!term)
|
|
1902
|
+
return "";
|
|
1903
|
+
if (term.t === "i")
|
|
1904
|
+
return term.i || "";
|
|
1905
|
+
if (term.t === "l")
|
|
1906
|
+
return term.v || "";
|
|
1907
|
+
if (term.t === "t" && term.tr) {
|
|
1908
|
+
// Quoted triple - return a serialized form
|
|
1909
|
+
const s = getTermValue$1(term.tr.s);
|
|
1910
|
+
const p = getTermValue$1(term.tr.p);
|
|
1911
|
+
const o = getTermValue$1(term.tr.o);
|
|
1912
|
+
return `<<${s} ${p} ${o}>>`;
|
|
1913
|
+
}
|
|
1914
|
+
return "";
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Extract quoted triple from a Term
|
|
1918
|
+
*/
|
|
1919
|
+
function extractQuotedTriple(term) {
|
|
1920
|
+
if (term.t === "t" && term.tr) {
|
|
1921
|
+
return {
|
|
1922
|
+
s: getTermValue$1(term.tr.s),
|
|
1923
|
+
p: getTermValue$1(term.tr.p),
|
|
1924
|
+
o: getTermValue$1(term.tr.o),
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Parse triples for a question event
|
|
1931
|
+
*/
|
|
1932
|
+
function parseQuestionTriples(explainId, explainGraph, triples) {
|
|
1933
|
+
const event = {
|
|
1934
|
+
type: "question",
|
|
1935
|
+
explainId,
|
|
1936
|
+
explainGraph,
|
|
1937
|
+
};
|
|
1938
|
+
for (const triple of triples) {
|
|
1939
|
+
const p = getTermValue$1(triple.p);
|
|
1940
|
+
const o = getTermValue$1(triple.o);
|
|
1941
|
+
if (p === client.TG_QUERY) {
|
|
1942
|
+
event.query = o;
|
|
1943
|
+
}
|
|
1944
|
+
else if (p === client.PROV_STARTED_AT_TIME) {
|
|
1945
|
+
event.timestamp = o;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return event;
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Parse triples for an exploration event
|
|
1952
|
+
*/
|
|
1953
|
+
function parseExplorationTriples(explainId, explainGraph, triples) {
|
|
1954
|
+
const event = {
|
|
1955
|
+
type: "exploration",
|
|
1956
|
+
explainId,
|
|
1957
|
+
explainGraph,
|
|
1958
|
+
};
|
|
1959
|
+
for (const triple of triples) {
|
|
1960
|
+
const p = getTermValue$1(triple.p);
|
|
1961
|
+
const o = getTermValue$1(triple.o);
|
|
1962
|
+
if (p === client.TG_EDGE_COUNT) {
|
|
1963
|
+
event.edgeCount = parseInt(o, 10);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return event;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Parse triples for a focus event
|
|
1970
|
+
*/
|
|
1971
|
+
function parseFocusTriples(explainId, explainGraph, triples) {
|
|
1972
|
+
const event = {
|
|
1973
|
+
type: "focus",
|
|
1974
|
+
explainId,
|
|
1975
|
+
explainGraph,
|
|
1976
|
+
edgeSelectionUris: [],
|
|
1977
|
+
};
|
|
1978
|
+
for (const triple of triples) {
|
|
1979
|
+
const p = getTermValue$1(triple.p);
|
|
1980
|
+
const o = getTermValue$1(triple.o);
|
|
1981
|
+
if (p === client.TG_SELECTED_EDGE && typeof o === "string") {
|
|
1982
|
+
event.edgeSelectionUris.push(o);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return event;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Parse triples for a synthesis event
|
|
1989
|
+
*/
|
|
1990
|
+
function parseSynthesisTriples(explainId, explainGraph, triples) {
|
|
1991
|
+
const event = {
|
|
1992
|
+
type: "synthesis",
|
|
1993
|
+
explainId,
|
|
1994
|
+
explainGraph,
|
|
1995
|
+
};
|
|
1996
|
+
for (const triple of triples) {
|
|
1997
|
+
const p = getTermValue$1(triple.p);
|
|
1998
|
+
const o = getTermValue$1(triple.o);
|
|
1999
|
+
if (p === client.TG_CONTENT) {
|
|
2000
|
+
event.contentLength = o.length;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return event;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Parse triples for an edge selection entity
|
|
2007
|
+
*/
|
|
2008
|
+
function parseEdgeSelectionTriples(triples) {
|
|
2009
|
+
let edge = null;
|
|
2010
|
+
let reasoning = null;
|
|
2011
|
+
for (const triple of triples) {
|
|
2012
|
+
const p = getTermValue$1(triple.p);
|
|
2013
|
+
if (p === client.TG_EDGE) {
|
|
2014
|
+
edge = extractQuotedTriple(triple.o);
|
|
2015
|
+
}
|
|
2016
|
+
else if (p === client.TG_REASONING) {
|
|
2017
|
+
reasoning = getTermValue$1(triple.o);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return { edge, reasoning };
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Parse triples based on event type
|
|
2024
|
+
*/
|
|
2025
|
+
function parseExplainTriples(explainId, explainGraph, triples) {
|
|
2026
|
+
const eventType = getEventType(explainId);
|
|
2027
|
+
switch (eventType) {
|
|
2028
|
+
case "question":
|
|
2029
|
+
return parseQuestionTriples(explainId, explainGraph, triples);
|
|
2030
|
+
case "exploration":
|
|
2031
|
+
return parseExplorationTriples(explainId, explainGraph, triples);
|
|
2032
|
+
case "focus":
|
|
2033
|
+
return parseFocusTriples(explainId, explainGraph, triples);
|
|
2034
|
+
case "synthesis":
|
|
2035
|
+
return parseSynthesisTriples(explainId, explainGraph, triples);
|
|
2036
|
+
default:
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* Hook for tracing provenance chains in the knowledge graph
|
|
2043
|
+
* Follows prov:wasDerivedFrom relationships from any entity to its source documents
|
|
2044
|
+
*/
|
|
2045
|
+
/**
|
|
2046
|
+
* Hook for tracing provenance chains
|
|
2047
|
+
*/
|
|
2048
|
+
const useProvenance = (options = {}) => {
|
|
2049
|
+
const { flow, collection = "default", maxDepth = 10 } = options;
|
|
2050
|
+
const socket = reactProvider.useSocket();
|
|
2051
|
+
const connectionState = reactProvider.useConnectionState();
|
|
2052
|
+
const sessionFlowId = useSessionStore((state) => state.flowId);
|
|
2053
|
+
const effectiveFlow = flow ?? sessionFlowId;
|
|
2054
|
+
const [isTracing, setIsTracing] = react.useState(false);
|
|
2055
|
+
// Label cache to avoid repeated queries
|
|
2056
|
+
const labelCacheRef = react.useRef(new Map());
|
|
2057
|
+
/**
|
|
2058
|
+
* Check if connected
|
|
2059
|
+
*/
|
|
2060
|
+
const isConnected = react.useCallback(() => {
|
|
2061
|
+
return (connectionState?.status === "authenticated" ||
|
|
2062
|
+
connectionState?.status === "unauthenticated");
|
|
2063
|
+
}, [connectionState]);
|
|
2064
|
+
/**
|
|
2065
|
+
* Query for rdfs:label of a URI
|
|
2066
|
+
*/
|
|
2067
|
+
const resolveLabel = react.useCallback(async (uri) => {
|
|
2068
|
+
// Check cache
|
|
2069
|
+
if (labelCacheRef.current.has(uri)) {
|
|
2070
|
+
return labelCacheRef.current.get(uri);
|
|
2071
|
+
}
|
|
2072
|
+
// Not an IRI - return as-is
|
|
2073
|
+
if (!uri.startsWith("http") && !uri.startsWith("urn:")) {
|
|
2074
|
+
return uri;
|
|
2075
|
+
}
|
|
2076
|
+
if (!isConnected()) {
|
|
2077
|
+
return uri;
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
const triples = await socket
|
|
2081
|
+
.flow(effectiveFlow)
|
|
2082
|
+
.triplesQuery({ t: "i", i: uri }, { t: "i", i: client.RDFS_LABEL }, undefined, 1, collection);
|
|
2083
|
+
const label = triples.length > 0 ? getTermValue$1(triples[0].o) : uri;
|
|
2084
|
+
// Cache the result
|
|
2085
|
+
labelCacheRef.current.set(uri, label);
|
|
2086
|
+
return label;
|
|
2087
|
+
}
|
|
2088
|
+
catch {
|
|
2089
|
+
return uri;
|
|
2090
|
+
}
|
|
2091
|
+
}, [socket, effectiveFlow, collection, isConnected]);
|
|
2092
|
+
/**
|
|
2093
|
+
* Query for prov:wasDerivedFrom parent of a URI
|
|
2094
|
+
*/
|
|
2095
|
+
const queryDerivedFrom = react.useCallback(async (uri) => {
|
|
2096
|
+
if (!isConnected())
|
|
2097
|
+
return null;
|
|
2098
|
+
try {
|
|
2099
|
+
const triples = await socket
|
|
2100
|
+
.flow(effectiveFlow)
|
|
2101
|
+
.triplesQuery({ t: "i", i: uri }, { t: "i", i: client.PROV_WAS_DERIVED_FROM }, undefined, 1, collection);
|
|
2102
|
+
if (triples.length > 0) {
|
|
2103
|
+
return getTermValue$1(triples[0].o);
|
|
2104
|
+
}
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
catch {
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
}, [socket, effectiveFlow, collection, isConnected]);
|
|
2111
|
+
/**
|
|
2112
|
+
* Trace the full provenance chain from a URI to root
|
|
2113
|
+
*/
|
|
2114
|
+
const traceChain = react.useCallback(async (uri) => {
|
|
2115
|
+
setIsTracing(true);
|
|
2116
|
+
try {
|
|
2117
|
+
const chain = [];
|
|
2118
|
+
let current = uri;
|
|
2119
|
+
for (let depth = 0; depth < maxDepth && current; depth++) {
|
|
2120
|
+
const label = await resolveLabel(current);
|
|
2121
|
+
chain.push({ uri: current, label });
|
|
2122
|
+
const parent = await queryDerivedFrom(current);
|
|
2123
|
+
if (!parent || parent === current) {
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
current = parent;
|
|
2127
|
+
}
|
|
2128
|
+
// The last item in the chain is the root document
|
|
2129
|
+
const rootItem = chain.length > 0 ? chain[chain.length - 1] : undefined;
|
|
2130
|
+
return {
|
|
2131
|
+
chain,
|
|
2132
|
+
documentUri: rootItem?.uri,
|
|
2133
|
+
documentLabel: rootItem?.label,
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
finally {
|
|
2137
|
+
setIsTracing(false);
|
|
2138
|
+
}
|
|
2139
|
+
}, [maxDepth, resolveLabel, queryDerivedFrom]);
|
|
2140
|
+
/**
|
|
2141
|
+
* Query for statements that reify an edge via tg:reifies
|
|
2142
|
+
*/
|
|
2143
|
+
const queryReifyingStatements = react.useCallback(async (s, p, o) => {
|
|
2144
|
+
if (!isConnected())
|
|
2145
|
+
return [];
|
|
2146
|
+
try {
|
|
2147
|
+
// Build the quoted triple term
|
|
2148
|
+
const quotedTriple = {
|
|
2149
|
+
t: "t",
|
|
2150
|
+
tr: {
|
|
2151
|
+
s: { t: "i", i: s },
|
|
2152
|
+
p: { t: "i", i: p },
|
|
2153
|
+
o: o.startsWith("http") || o.startsWith("urn:")
|
|
2154
|
+
? { t: "i", i: o }
|
|
2155
|
+
: { t: "l", v: o },
|
|
2156
|
+
},
|
|
2157
|
+
};
|
|
2158
|
+
const triples = await socket
|
|
2159
|
+
.flow(effectiveFlow)
|
|
2160
|
+
.triplesQuery(undefined, { t: "i", i: client.TG_REIFIES }, quotedTriple, 10, collection);
|
|
2161
|
+
return triples.map((t) => getTermValue$1(t.s));
|
|
2162
|
+
}
|
|
2163
|
+
catch {
|
|
2164
|
+
return [];
|
|
2165
|
+
}
|
|
2166
|
+
}, [socket, effectiveFlow, collection, isConnected]);
|
|
2167
|
+
/**
|
|
2168
|
+
* Trace provenance for an edge - finds reifying statements and traces each
|
|
2169
|
+
*/
|
|
2170
|
+
const traceEdgeProvenance = react.useCallback(async (s, p, o) => {
|
|
2171
|
+
setIsTracing(true);
|
|
2172
|
+
try {
|
|
2173
|
+
// Find statements that reify this edge
|
|
2174
|
+
const stmtUris = await queryReifyingStatements(s, p, o);
|
|
2175
|
+
// For each reifying statement, trace its provenance chain
|
|
2176
|
+
const chains = [];
|
|
2177
|
+
for (const stmtUri of stmtUris) {
|
|
2178
|
+
// Get the wasDerivedFrom source for this statement
|
|
2179
|
+
const sourceUri = await queryDerivedFrom(stmtUri);
|
|
2180
|
+
if (sourceUri) {
|
|
2181
|
+
const chain = await traceChain(sourceUri);
|
|
2182
|
+
chains.push(chain);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
return chains;
|
|
2186
|
+
}
|
|
2187
|
+
finally {
|
|
2188
|
+
setIsTracing(false);
|
|
2189
|
+
}
|
|
2190
|
+
}, [queryReifyingStatements, queryDerivedFrom, traceChain]);
|
|
2191
|
+
/**
|
|
2192
|
+
* Clear the label cache
|
|
2193
|
+
*/
|
|
2194
|
+
const clearCache = react.useCallback(() => {
|
|
2195
|
+
labelCacheRef.current.clear();
|
|
2196
|
+
}, []);
|
|
2197
|
+
return {
|
|
2198
|
+
traceChain,
|
|
2199
|
+
traceEdgeProvenance,
|
|
2200
|
+
resolveLabel,
|
|
2201
|
+
clearCache,
|
|
2202
|
+
isTracing,
|
|
2203
|
+
};
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Hook for managing explainability state during GraphRAG queries
|
|
2208
|
+
* Unpacks explain events into structured data with provenance chains
|
|
2209
|
+
*
|
|
2210
|
+
* Processing strategy:
|
|
2211
|
+
* - Events are processed immediately as they arrive
|
|
2212
|
+
* - Main event nodes (question, exploration, focus, synthesis) use
|
|
2213
|
+
* stability-based retry: fetch until count > 0 AND stable
|
|
2214
|
+
* - Edge sub-objects use simple retry-until-non-empty (fast)
|
|
2215
|
+
* - All edge fetching + label resolution + provenance runs in parallel
|
|
2216
|
+
*/
|
|
2217
|
+
/**
|
|
2218
|
+
* Hook for managing explainability during inference
|
|
2219
|
+
*/
|
|
2220
|
+
const useExplainability = (options = {}) => {
|
|
2221
|
+
const { flow, collection = "default", traceProvenance = true, } = options;
|
|
2222
|
+
const socket = reactProvider.useSocket();
|
|
2223
|
+
const connectionState = reactProvider.useConnectionState();
|
|
2224
|
+
const sessionFlowId = useSessionStore((state) => state.flowId);
|
|
2225
|
+
const effectiveFlow = flow ?? sessionFlowId;
|
|
2226
|
+
const { traceEdgeProvenance, resolveLabel } = useProvenance({
|
|
2227
|
+
flow: effectiveFlow,
|
|
2228
|
+
collection,
|
|
2229
|
+
});
|
|
2230
|
+
const [events, setEvents] = react.useState([]);
|
|
2231
|
+
const [session, setSession] = react.useState({});
|
|
2232
|
+
const [isUnpacking, setIsUnpacking] = react.useState(false);
|
|
2233
|
+
const [error, setError] = react.useState(null);
|
|
2234
|
+
// Mirror session in a ref so it's always immediately readable (no render delay)
|
|
2235
|
+
const sessionRef = react.useRef({});
|
|
2236
|
+
// Track pending unpack operations
|
|
2237
|
+
const unpackQueueRef = react.useRef([]);
|
|
2238
|
+
const isProcessingRef = react.useRef(false);
|
|
2239
|
+
/**
|
|
2240
|
+
* Check if connected
|
|
2241
|
+
*/
|
|
2242
|
+
const isConnected = react.useCallback(() => {
|
|
2243
|
+
return (connectionState?.status === "authenticated" ||
|
|
2244
|
+
connectionState?.status === "unauthenticated");
|
|
2245
|
+
}, [connectionState]);
|
|
2246
|
+
/**
|
|
2247
|
+
* Single triple query (no retry)
|
|
2248
|
+
*/
|
|
2249
|
+
const fetchTriples = react.useCallback(async (explainId, explainGraph) => {
|
|
2250
|
+
return socket
|
|
2251
|
+
.flow(effectiveFlow)
|
|
2252
|
+
.triplesQuery({ t: "i", i: explainId }, undefined, undefined, 100, collection, explainGraph);
|
|
2253
|
+
}, [socket, effectiveFlow, collection]);
|
|
2254
|
+
/**
|
|
2255
|
+
* Stability-based retry for main event nodes.
|
|
2256
|
+
* Retries until: count > 0 AND count matches previous fetch.
|
|
2257
|
+
* Used for question, exploration, focus, synthesis nodes where the
|
|
2258
|
+
* backend may write multiple triples incrementally.
|
|
2259
|
+
*/
|
|
2260
|
+
const queryWithStabilityRetry = react.useCallback(async (explainId, explainGraph, timeoutMs = 5000) => {
|
|
2261
|
+
if (!isConnected())
|
|
2262
|
+
return [];
|
|
2263
|
+
const retryDelay = 500;
|
|
2264
|
+
const maxAttempts = Math.ceil(timeoutMs / retryDelay) + 1;
|
|
2265
|
+
let prevCount = -1;
|
|
2266
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2267
|
+
try {
|
|
2268
|
+
const triples = await fetchTriples(explainId, explainGraph);
|
|
2269
|
+
const count = triples.length;
|
|
2270
|
+
if (count > 0 && count === prevCount) {
|
|
2271
|
+
return triples;
|
|
2272
|
+
}
|
|
2273
|
+
prevCount = count;
|
|
2274
|
+
if (attempt < maxAttempts - 1) {
|
|
2275
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
catch (err) {
|
|
2279
|
+
console.error("[explain] triple query failed:", explainId, err);
|
|
2280
|
+
return [];
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
// Return last fetch if we got anything
|
|
2284
|
+
if (prevCount > 0) {
|
|
2285
|
+
try {
|
|
2286
|
+
return await fetchTriples(explainId, explainGraph);
|
|
2287
|
+
}
|
|
2288
|
+
catch {
|
|
2289
|
+
return [];
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return [];
|
|
2293
|
+
}, [fetchTriples, isConnected]);
|
|
2294
|
+
/**
|
|
2295
|
+
* Simple retry-until-non-empty for sub-objects (edge selections).
|
|
2296
|
+
* These are small atomic writes — either fully there or not yet.
|
|
2297
|
+
*/
|
|
2298
|
+
const queryWithSimpleRetry = react.useCallback(async (explainId, explainGraph, timeoutMs = 5000) => {
|
|
2299
|
+
if (!isConnected())
|
|
2300
|
+
return [];
|
|
2301
|
+
const retryDelay = 300;
|
|
2302
|
+
const maxAttempts = Math.ceil(timeoutMs / retryDelay) + 1;
|
|
2303
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2304
|
+
try {
|
|
2305
|
+
const triples = await fetchTriples(explainId, explainGraph);
|
|
2306
|
+
if (triples.length > 0)
|
|
2307
|
+
return triples;
|
|
2308
|
+
if (attempt < maxAttempts - 1) {
|
|
2309
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
catch (err) {
|
|
2313
|
+
console.error("[explain] triple query failed:", explainId, err);
|
|
2314
|
+
return [];
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return [];
|
|
2318
|
+
}, [fetchTriples, isConnected]);
|
|
2319
|
+
/**
|
|
2320
|
+
* Resolve a single edge: fetch triples, labels, and provenance in parallel
|
|
2321
|
+
*/
|
|
2322
|
+
const resolveEdge = react.useCallback(async (edgeSelUri, explainGraph) => {
|
|
2323
|
+
const triples = await queryWithSimpleRetry(edgeSelUri, explainGraph);
|
|
2324
|
+
const { edge, reasoning } = parseEdgeSelectionTriples(triples);
|
|
2325
|
+
if (!edge)
|
|
2326
|
+
return null;
|
|
2327
|
+
const selectedEdge = {
|
|
2328
|
+
edge,
|
|
2329
|
+
reasoning: reasoning || undefined,
|
|
2330
|
+
};
|
|
2331
|
+
// Kick off labels and provenance in parallel
|
|
2332
|
+
const [labels, provenanceChains] = await Promise.all([
|
|
2333
|
+
// Labels — all 3 in parallel
|
|
2334
|
+
Promise.all([
|
|
2335
|
+
resolveLabel(edge.s),
|
|
2336
|
+
resolveLabel(edge.p),
|
|
2337
|
+
resolveLabel(edge.o),
|
|
2338
|
+
]),
|
|
2339
|
+
// Provenance
|
|
2340
|
+
traceProvenance
|
|
2341
|
+
? traceEdgeProvenance(edge.s, edge.p, edge.o)
|
|
2342
|
+
: Promise.resolve([]),
|
|
2343
|
+
]);
|
|
2344
|
+
selectedEdge.labels = { s: labels[0], p: labels[1], o: labels[2] };
|
|
2345
|
+
if (provenanceChains.length > 0) {
|
|
2346
|
+
selectedEdge.sources = provenanceChains.map((c) => c.chain).flat();
|
|
2347
|
+
}
|
|
2348
|
+
return selectedEdge;
|
|
2349
|
+
}, [queryWithSimpleRetry, resolveLabel, traceProvenance, traceEdgeProvenance]);
|
|
2350
|
+
/**
|
|
2351
|
+
* Unpack a focus event — resolve ALL edges in parallel
|
|
2352
|
+
*/
|
|
2353
|
+
const unpackFocusEvent = react.useCallback(async (focusEvent) => {
|
|
2354
|
+
// Fire off all edge resolutions concurrently
|
|
2355
|
+
const edgePromises = focusEvent.edgeSelectionUris.map((uri) => resolveEdge(uri, focusEvent.explainGraph));
|
|
2356
|
+
const results = await Promise.all(edgePromises);
|
|
2357
|
+
const selectedEdges = results.filter((e) => e !== null);
|
|
2358
|
+
return {
|
|
2359
|
+
...focusEvent,
|
|
2360
|
+
selectedEdges,
|
|
2361
|
+
};
|
|
2362
|
+
}, [resolveEdge]);
|
|
2363
|
+
/** Helper to update both state and ref together */
|
|
2364
|
+
const updateSession = react.useCallback((updater) => {
|
|
2365
|
+
sessionRef.current = updater(sessionRef.current);
|
|
2366
|
+
setSession(updater);
|
|
2367
|
+
}, []);
|
|
2368
|
+
/**
|
|
2369
|
+
* Process a single explain event
|
|
2370
|
+
*/
|
|
2371
|
+
const processEvent = react.useCallback(async (event) => {
|
|
2372
|
+
// Query triples for main event node (stability retry)
|
|
2373
|
+
const triples = await queryWithStabilityRetry(event.explainId, event.explainGraph);
|
|
2374
|
+
// Parse into structured data
|
|
2375
|
+
const parsed = parseExplainTriples(event.explainId, event.explainGraph, triples);
|
|
2376
|
+
if (!parsed)
|
|
2377
|
+
return;
|
|
2378
|
+
// Update session based on event type
|
|
2379
|
+
updateSession((prev) => {
|
|
2380
|
+
const next = { ...prev };
|
|
2381
|
+
switch (parsed.type) {
|
|
2382
|
+
case "question":
|
|
2383
|
+
next.question = parsed;
|
|
2384
|
+
break;
|
|
2385
|
+
case "exploration":
|
|
2386
|
+
next.exploration = parsed;
|
|
2387
|
+
break;
|
|
2388
|
+
case "focus":
|
|
2389
|
+
// Will be updated again after unpacking
|
|
2390
|
+
next.focus = parsed;
|
|
2391
|
+
break;
|
|
2392
|
+
case "synthesis":
|
|
2393
|
+
next.synthesis = parsed;
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
return next;
|
|
2397
|
+
});
|
|
2398
|
+
// For focus events, unpack edges in parallel
|
|
2399
|
+
if (parsed.type === "focus") {
|
|
2400
|
+
const unpackedFocus = await unpackFocusEvent(parsed);
|
|
2401
|
+
updateSession((prev) => ({
|
|
2402
|
+
...prev,
|
|
2403
|
+
focus: unpackedFocus,
|
|
2404
|
+
}));
|
|
2405
|
+
}
|
|
2406
|
+
}, [queryWithStabilityRetry, unpackFocusEvent, updateSession]);
|
|
2407
|
+
/**
|
|
2408
|
+
* Process the unpack queue
|
|
2409
|
+
*/
|
|
2410
|
+
const processQueue = react.useCallback(async () => {
|
|
2411
|
+
if (isProcessingRef.current)
|
|
2412
|
+
return;
|
|
2413
|
+
if (unpackQueueRef.current.length === 0)
|
|
2414
|
+
return;
|
|
2415
|
+
isProcessingRef.current = true;
|
|
2416
|
+
setIsUnpacking(true);
|
|
2417
|
+
try {
|
|
2418
|
+
while (unpackQueueRef.current.length > 0) {
|
|
2419
|
+
const event = unpackQueueRef.current.shift();
|
|
2420
|
+
await processEvent(event);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
catch (err) {
|
|
2424
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2425
|
+
setError(msg);
|
|
2426
|
+
}
|
|
2427
|
+
isProcessingRef.current = false;
|
|
2428
|
+
setIsUnpacking(false);
|
|
2429
|
+
// Check if more events were queued during processing (e.g., after a reset)
|
|
2430
|
+
if (unpackQueueRef.current.length > 0) {
|
|
2431
|
+
processQueue();
|
|
2432
|
+
}
|
|
2433
|
+
}, [processEvent]);
|
|
2434
|
+
/**
|
|
2435
|
+
* Add an explain event — immediately queues and starts processing
|
|
2436
|
+
*/
|
|
2437
|
+
const addEvent = react.useCallback((event) => {
|
|
2438
|
+
setEvents((prev) => [...prev, event]);
|
|
2439
|
+
unpackQueueRef.current.push(event);
|
|
2440
|
+
processQueue();
|
|
2441
|
+
}, [processQueue]);
|
|
2442
|
+
/**
|
|
2443
|
+
* Reset the session
|
|
2444
|
+
*/
|
|
2445
|
+
const reset = react.useCallback(() => {
|
|
2446
|
+
setEvents([]);
|
|
2447
|
+
setSession({});
|
|
2448
|
+
sessionRef.current = {};
|
|
2449
|
+
setError(null);
|
|
2450
|
+
unpackQueueRef.current = [];
|
|
2451
|
+
}, []);
|
|
2452
|
+
return {
|
|
2453
|
+
addEvent,
|
|
2454
|
+
session,
|
|
2455
|
+
sessionRef,
|
|
2456
|
+
events,
|
|
2457
|
+
isUnpacking,
|
|
2458
|
+
isProcessingRef,
|
|
2459
|
+
error,
|
|
2460
|
+
reset,
|
|
2461
|
+
};
|
|
2462
|
+
};
|
|
2463
|
+
|
|
1835
2464
|
/**
|
|
1836
2465
|
* High-level hook for managing chat sessions
|
|
1837
2466
|
* Combines conversation state with inference services
|
|
@@ -1855,6 +2484,19 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1855
2484
|
const effectiveFlow = flow ?? sessionFlowId;
|
|
1856
2485
|
// Settings for GraphRAG configuration
|
|
1857
2486
|
const { settings } = useSettings();
|
|
2487
|
+
// Explainability store for persisting sessions
|
|
2488
|
+
const addExplainSession = useExplainabilityStore((state) => state.addSession);
|
|
2489
|
+
// Explainability hook for processing events (processes each event immediately)
|
|
2490
|
+
const explainability = useExplainability({
|
|
2491
|
+
flow: effectiveFlow,
|
|
2492
|
+
collection: settings.collection,
|
|
2493
|
+
});
|
|
2494
|
+
const explainabilityRef = react.useRef(explainability);
|
|
2495
|
+
explainabilityRef.current = explainability;
|
|
2496
|
+
// Generate unique session IDs
|
|
2497
|
+
const generateSessionId = react.useCallback(() => {
|
|
2498
|
+
return `explain-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2499
|
+
}, []);
|
|
1858
2500
|
// Inference services
|
|
1859
2501
|
const inference = useInference({ flow });
|
|
1860
2502
|
/**
|
|
@@ -1866,6 +2508,13 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1866
2508
|
addActivity(ragActivity);
|
|
1867
2509
|
let accumulated = "";
|
|
1868
2510
|
let messageAdded = false;
|
|
2511
|
+
// Check if explainability is enabled
|
|
2512
|
+
const explainabilityEnabled = settings.featureSwitches.explainability;
|
|
2513
|
+
const sessionId = explainabilityEnabled ? generateSessionId() : undefined;
|
|
2514
|
+
// Reset explainability state for new query
|
|
2515
|
+
if (explainabilityEnabled) {
|
|
2516
|
+
explainabilityRef.current.reset();
|
|
2517
|
+
}
|
|
1869
2518
|
try {
|
|
1870
2519
|
// Execute Graph RAG with streaming and entity discovery
|
|
1871
2520
|
const result = await inference.graphRag({
|
|
@@ -1881,8 +2530,8 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1881
2530
|
onChunk: (chunk, complete) => {
|
|
1882
2531
|
accumulated += chunk;
|
|
1883
2532
|
if (!messageAdded) {
|
|
1884
|
-
// Add empty message on first chunk
|
|
1885
|
-
addMessage("ai", accumulated);
|
|
2533
|
+
// Add empty message on first chunk (with session ID if enabled)
|
|
2534
|
+
addMessage("ai", accumulated, undefined, sessionId);
|
|
1886
2535
|
messageAdded = true;
|
|
1887
2536
|
}
|
|
1888
2537
|
else {
|
|
@@ -1890,9 +2539,32 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1890
2539
|
updateLastMessage(accumulated);
|
|
1891
2540
|
}
|
|
1892
2541
|
},
|
|
2542
|
+
// Wire up explainability callback if feature is enabled
|
|
2543
|
+
...(explainabilityEnabled && {
|
|
2544
|
+
onExplain: (event) => {
|
|
2545
|
+
console.log("[explain] event received:", event.explainId);
|
|
2546
|
+
explainabilityRef.current.addEvent(event);
|
|
2547
|
+
},
|
|
2548
|
+
}),
|
|
1893
2549
|
},
|
|
1894
2550
|
});
|
|
1895
2551
|
removeActivity(ragActivity);
|
|
2552
|
+
// Store explainability session progressively — events are already being
|
|
2553
|
+
// processed as they arrive. Wait for processing to finish, then snapshot.
|
|
2554
|
+
if (explainabilityEnabled && sessionId) {
|
|
2555
|
+
const waitAndStore = async () => {
|
|
2556
|
+
// Poll until processing completes (events are processed as they arrive)
|
|
2557
|
+
const maxWait = 30000;
|
|
2558
|
+
let elapsed = 0;
|
|
2559
|
+
while (explainabilityRef.current.isProcessingRef.current && elapsed < maxWait) {
|
|
2560
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2561
|
+
elapsed += 500;
|
|
2562
|
+
}
|
|
2563
|
+
const sess = explainabilityRef.current.sessionRef.current;
|
|
2564
|
+
addExplainSession(sessionId, sess);
|
|
2565
|
+
};
|
|
2566
|
+
waitAndStore();
|
|
2567
|
+
}
|
|
1896
2568
|
// Start embeddings activity
|
|
1897
2569
|
addActivity(embActivity);
|
|
1898
2570
|
// Get labels for each entity
|
|
@@ -1900,7 +2572,7 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1900
2572
|
.filter((match) => match.entity !== null)
|
|
1901
2573
|
.map(async (match) => {
|
|
1902
2574
|
const entity = match.entity;
|
|
1903
|
-
const labelActivity = "Label " + getTermValue$
|
|
2575
|
+
const labelActivity = "Label " + getTermValue$3(entity);
|
|
1904
2576
|
addActivity(labelActivity);
|
|
1905
2577
|
try {
|
|
1906
2578
|
const triples = await socket
|
|
@@ -1919,8 +2591,8 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1919
2591
|
const entityList = labelResponses
|
|
1920
2592
|
.filter((resp) => resp && resp.length > 0)
|
|
1921
2593
|
.map((resp) => ({
|
|
1922
|
-
label: getTermValue$
|
|
1923
|
-
uri: getTermValue$
|
|
2594
|
+
label: getTermValue$3(resp[0].o),
|
|
2595
|
+
uri: getTermValue$3(resp[0].s),
|
|
1924
2596
|
}));
|
|
1925
2597
|
setEntities(entityList);
|
|
1926
2598
|
removeActivity(embActivity);
|
|
@@ -4642,6 +5314,623 @@ const useNodeDetails = (nodeId, flowId) => {
|
|
|
4642
5314
|
};
|
|
4643
5315
|
};
|
|
4644
5316
|
|
|
5317
|
+
// Default chunk size: 5MB (matches backend default)
|
|
5318
|
+
const DEFAULT_CHUNK_SIZE$1 = 5 * 1024 * 1024;
|
|
5319
|
+
// Maximum parallel chunk uploads
|
|
5320
|
+
const DEFAULT_PARALLEL_UPLOADS = 3;
|
|
5321
|
+
/**
|
|
5322
|
+
* Hook for managing chunked document uploads with progress tracking
|
|
5323
|
+
*
|
|
5324
|
+
* Features:
|
|
5325
|
+
* - Automatic chunking of large files
|
|
5326
|
+
* - Parallel chunk uploads for performance
|
|
5327
|
+
* - Progress tracking (bytes, percentage, chunks)
|
|
5328
|
+
* - Pause/resume support
|
|
5329
|
+
* - Cancel support
|
|
5330
|
+
* - Resumability after interruption
|
|
5331
|
+
*
|
|
5332
|
+
* @param options - Configuration options for the upload
|
|
5333
|
+
* @returns Upload state and control methods
|
|
5334
|
+
*/
|
|
5335
|
+
const useChunkedUpload = (options = {}) => {
|
|
5336
|
+
const { chunkSize = DEFAULT_CHUNK_SIZE$1, parallelUploads = DEFAULT_PARALLEL_UPLOADS, onProgress, onComplete, onError, } = options;
|
|
5337
|
+
const socket = reactProvider.useSocket();
|
|
5338
|
+
const connectionState = reactProvider.useConnectionState();
|
|
5339
|
+
const queryClient = reactQuery.useQueryClient();
|
|
5340
|
+
const notify = useNotification();
|
|
5341
|
+
// Upload state
|
|
5342
|
+
const [progress, setProgress] = react.useState({
|
|
5343
|
+
totalBytes: 0,
|
|
5344
|
+
bytesUploaded: 0,
|
|
5345
|
+
percentage: 0,
|
|
5346
|
+
totalChunks: 0,
|
|
5347
|
+
chunksUploaded: 0,
|
|
5348
|
+
pendingChunks: [],
|
|
5349
|
+
status: "idle",
|
|
5350
|
+
});
|
|
5351
|
+
// Refs for managing upload lifecycle
|
|
5352
|
+
const abortControllerRef = react.useRef(null);
|
|
5353
|
+
const isPausedRef = react.useRef(false);
|
|
5354
|
+
const currentFileRef = react.useRef(null);
|
|
5355
|
+
const uploadIdRef = react.useRef(null);
|
|
5356
|
+
// Update progress and notify callback
|
|
5357
|
+
const updateProgress = react.useCallback((updates) => {
|
|
5358
|
+
setProgress((prev) => {
|
|
5359
|
+
const next = { ...prev, ...updates };
|
|
5360
|
+
onProgress?.(next);
|
|
5361
|
+
return next;
|
|
5362
|
+
});
|
|
5363
|
+
}, [onProgress]);
|
|
5364
|
+
// Read a chunk from the file as base64
|
|
5365
|
+
const readChunkAsBase64 = react.useCallback(async (file, chunkIndex, chunkSz) => {
|
|
5366
|
+
const start = chunkIndex * chunkSz;
|
|
5367
|
+
const end = Math.min(start + chunkSz, file.size);
|
|
5368
|
+
const blob = file.slice(start, end);
|
|
5369
|
+
return new Promise((resolve, reject) => {
|
|
5370
|
+
const reader = new FileReader();
|
|
5371
|
+
reader.onloadend = () => {
|
|
5372
|
+
const dataUrl = reader.result;
|
|
5373
|
+
// Extract base64 from data URL
|
|
5374
|
+
const base64 = dataUrl.replace(/^data:[^;]+;base64,/, "");
|
|
5375
|
+
resolve(base64);
|
|
5376
|
+
};
|
|
5377
|
+
reader.onerror = () => reject(new Error("Failed to read file chunk"));
|
|
5378
|
+
reader.readAsDataURL(blob);
|
|
5379
|
+
});
|
|
5380
|
+
}, []);
|
|
5381
|
+
// Upload chunks in parallel with concurrency limit
|
|
5382
|
+
const uploadChunks = react.useCallback(async (file, uploadId, pendingChunks, chunkSz, totalChunks) => {
|
|
5383
|
+
const remaining = [...pendingChunks];
|
|
5384
|
+
let completedCount = totalChunks - remaining.length;
|
|
5385
|
+
let bytesUploaded = completedCount * chunkSz;
|
|
5386
|
+
// Process chunks with limited parallelism
|
|
5387
|
+
const uploadNextBatch = async () => {
|
|
5388
|
+
while (remaining.length > 0 && !abortControllerRef.current?.signal.aborted) {
|
|
5389
|
+
// Wait if paused
|
|
5390
|
+
if (isPausedRef.current) {
|
|
5391
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5392
|
+
continue;
|
|
5393
|
+
}
|
|
5394
|
+
// Take up to parallelUploads chunks
|
|
5395
|
+
const batch = remaining.splice(0, parallelUploads);
|
|
5396
|
+
// Upload batch in parallel
|
|
5397
|
+
const results = await Promise.allSettled(batch.map(async (chunkIndex) => {
|
|
5398
|
+
const content = await readChunkAsBase64(file, chunkIndex, chunkSz);
|
|
5399
|
+
await socket.librarian().uploadChunk(uploadId, chunkIndex, content);
|
|
5400
|
+
return chunkIndex;
|
|
5401
|
+
}));
|
|
5402
|
+
// Process results
|
|
5403
|
+
for (const result of results) {
|
|
5404
|
+
if (result.status === "fulfilled") {
|
|
5405
|
+
completedCount++;
|
|
5406
|
+
// Calculate actual bytes for this chunk
|
|
5407
|
+
const chunkIdx = result.value;
|
|
5408
|
+
const chunkStart = chunkIdx * chunkSz;
|
|
5409
|
+
const chunkEnd = Math.min(chunkStart + chunkSz, file.size);
|
|
5410
|
+
bytesUploaded += chunkEnd - chunkStart;
|
|
5411
|
+
updateProgress({
|
|
5412
|
+
chunksUploaded: completedCount,
|
|
5413
|
+
bytesUploaded,
|
|
5414
|
+
percentage: Math.round((bytesUploaded / file.size) * 100),
|
|
5415
|
+
pendingChunks: [...remaining],
|
|
5416
|
+
});
|
|
5417
|
+
}
|
|
5418
|
+
else {
|
|
5419
|
+
// Re-add failed chunk to retry
|
|
5420
|
+
const failedIndex = batch[results.indexOf(result)];
|
|
5421
|
+
remaining.push(failedIndex);
|
|
5422
|
+
console.warn(`Chunk ${failedIndex} failed, will retry:`, result.reason);
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
};
|
|
5427
|
+
await uploadNextBatch();
|
|
5428
|
+
}, [socket, parallelUploads, readChunkAsBase64, updateProgress]);
|
|
5429
|
+
/**
|
|
5430
|
+
* Start a new chunked upload
|
|
5431
|
+
*/
|
|
5432
|
+
const upload = react.useCallback(async (params) => {
|
|
5433
|
+
const { file, title, comments = "", tags = [], collection, documentId } = params;
|
|
5434
|
+
// Validate connection
|
|
5435
|
+
if (connectionState?.status !== "authenticated" &&
|
|
5436
|
+
connectionState?.status !== "unauthenticated") {
|
|
5437
|
+
const error = "Not connected to server";
|
|
5438
|
+
updateProgress({ status: "error", error });
|
|
5439
|
+
onError?.(error);
|
|
5440
|
+
return null;
|
|
5441
|
+
}
|
|
5442
|
+
// Reset state
|
|
5443
|
+
abortControllerRef.current = new AbortController();
|
|
5444
|
+
isPausedRef.current = false;
|
|
5445
|
+
currentFileRef.current = file;
|
|
5446
|
+
const docId = documentId || createDocId();
|
|
5447
|
+
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
5448
|
+
const pendingChunks = Array.from({ length: totalChunks }, (_, i) => i);
|
|
5449
|
+
updateProgress({
|
|
5450
|
+
totalBytes: file.size,
|
|
5451
|
+
bytesUploaded: 0,
|
|
5452
|
+
percentage: 0,
|
|
5453
|
+
totalChunks,
|
|
5454
|
+
chunksUploaded: 0,
|
|
5455
|
+
pendingChunks,
|
|
5456
|
+
status: "preparing",
|
|
5457
|
+
error: undefined,
|
|
5458
|
+
uploadId: undefined,
|
|
5459
|
+
documentId: undefined,
|
|
5460
|
+
});
|
|
5461
|
+
try {
|
|
5462
|
+
// Initialize upload session
|
|
5463
|
+
const metadata = {
|
|
5464
|
+
id: docId,
|
|
5465
|
+
time: Math.floor(Date.now() / 1000),
|
|
5466
|
+
kind: file.type || "application/octet-stream",
|
|
5467
|
+
title,
|
|
5468
|
+
comments,
|
|
5469
|
+
user: "trustgraph", // Will be set by server based on auth
|
|
5470
|
+
collection: collection || "default",
|
|
5471
|
+
tags,
|
|
5472
|
+
};
|
|
5473
|
+
const beginResponse = await socket
|
|
5474
|
+
.librarian()
|
|
5475
|
+
.beginUpload(metadata, file.size, chunkSize);
|
|
5476
|
+
const uploadId = beginResponse["upload-id"];
|
|
5477
|
+
uploadIdRef.current = uploadId;
|
|
5478
|
+
updateProgress({
|
|
5479
|
+
status: "uploading",
|
|
5480
|
+
uploadId,
|
|
5481
|
+
});
|
|
5482
|
+
// Upload all chunks
|
|
5483
|
+
await uploadChunks(file, uploadId, pendingChunks, chunkSize, totalChunks);
|
|
5484
|
+
// Check if cancelled
|
|
5485
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
5486
|
+
return null;
|
|
5487
|
+
}
|
|
5488
|
+
// Complete the upload
|
|
5489
|
+
updateProgress({ status: "completing" });
|
|
5490
|
+
const completeResponse = await socket.librarian().completeUpload(uploadId);
|
|
5491
|
+
const finalDocId = completeResponse["document-id"];
|
|
5492
|
+
updateProgress({
|
|
5493
|
+
status: "completed",
|
|
5494
|
+
documentId: finalDocId,
|
|
5495
|
+
percentage: 100,
|
|
5496
|
+
});
|
|
5497
|
+
// Invalidate documents cache
|
|
5498
|
+
queryClient.invalidateQueries({ queryKey: ["documents"] });
|
|
5499
|
+
notify.success(`Upload complete: ${title}`);
|
|
5500
|
+
onComplete?.(finalDocId);
|
|
5501
|
+
return finalDocId;
|
|
5502
|
+
}
|
|
5503
|
+
catch (err) {
|
|
5504
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
5505
|
+
updateProgress({ status: "error", error: errorMsg });
|
|
5506
|
+
notify.error(`Upload failed: ${errorMsg}`);
|
|
5507
|
+
onError?.(errorMsg);
|
|
5508
|
+
return null;
|
|
5509
|
+
}
|
|
5510
|
+
}, [
|
|
5511
|
+
socket,
|
|
5512
|
+
connectionState,
|
|
5513
|
+
chunkSize,
|
|
5514
|
+
queryClient,
|
|
5515
|
+
notify,
|
|
5516
|
+
updateProgress,
|
|
5517
|
+
uploadChunks,
|
|
5518
|
+
onComplete,
|
|
5519
|
+
onError,
|
|
5520
|
+
]);
|
|
5521
|
+
/**
|
|
5522
|
+
* Resume an interrupted upload
|
|
5523
|
+
*/
|
|
5524
|
+
const resume = react.useCallback(async (params) => {
|
|
5525
|
+
const { uploadId, file } = params;
|
|
5526
|
+
// Validate connection
|
|
5527
|
+
if (connectionState?.status !== "authenticated" &&
|
|
5528
|
+
connectionState?.status !== "unauthenticated") {
|
|
5529
|
+
const error = "Not connected to server";
|
|
5530
|
+
updateProgress({ status: "error", error });
|
|
5531
|
+
onError?.(error);
|
|
5532
|
+
return null;
|
|
5533
|
+
}
|
|
5534
|
+
abortControllerRef.current = new AbortController();
|
|
5535
|
+
isPausedRef.current = false;
|
|
5536
|
+
currentFileRef.current = file;
|
|
5537
|
+
uploadIdRef.current = uploadId;
|
|
5538
|
+
updateProgress({
|
|
5539
|
+
status: "preparing",
|
|
5540
|
+
uploadId,
|
|
5541
|
+
});
|
|
5542
|
+
try {
|
|
5543
|
+
// Get current upload status
|
|
5544
|
+
const status = await socket.librarian().getUploadStatus(uploadId);
|
|
5545
|
+
if (status["upload-state"] === "completed") {
|
|
5546
|
+
updateProgress({ status: "completed" });
|
|
5547
|
+
return null;
|
|
5548
|
+
}
|
|
5549
|
+
if (status["upload-state"] === "expired") {
|
|
5550
|
+
throw new Error("Upload session has expired");
|
|
5551
|
+
}
|
|
5552
|
+
const totalChunks = status["total-chunks"];
|
|
5553
|
+
const missingChunks = status["missing-chunks"];
|
|
5554
|
+
const bytesReceived = status["bytes-received"];
|
|
5555
|
+
const totalBytes = status["total-bytes"];
|
|
5556
|
+
updateProgress({
|
|
5557
|
+
totalBytes,
|
|
5558
|
+
bytesUploaded: bytesReceived,
|
|
5559
|
+
percentage: Math.round((bytesReceived / totalBytes) * 100),
|
|
5560
|
+
totalChunks,
|
|
5561
|
+
chunksUploaded: totalChunks - missingChunks.length,
|
|
5562
|
+
pendingChunks: missingChunks,
|
|
5563
|
+
status: "uploading",
|
|
5564
|
+
});
|
|
5565
|
+
// Upload missing chunks
|
|
5566
|
+
const effectiveChunkSize = status["chunk-size"] || chunkSize;
|
|
5567
|
+
await uploadChunks(file, uploadId, missingChunks, effectiveChunkSize, totalChunks);
|
|
5568
|
+
// Check if cancelled
|
|
5569
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
5570
|
+
return null;
|
|
5571
|
+
}
|
|
5572
|
+
// Complete the upload
|
|
5573
|
+
updateProgress({ status: "completing" });
|
|
5574
|
+
const completeResponse = await socket.librarian().completeUpload(uploadId);
|
|
5575
|
+
const finalDocId = completeResponse["document-id"];
|
|
5576
|
+
updateProgress({
|
|
5577
|
+
status: "completed",
|
|
5578
|
+
documentId: finalDocId,
|
|
5579
|
+
percentage: 100,
|
|
5580
|
+
});
|
|
5581
|
+
// Invalidate documents cache
|
|
5582
|
+
queryClient.invalidateQueries({ queryKey: ["documents"] });
|
|
5583
|
+
notify.success("Upload resumed and completed");
|
|
5584
|
+
onComplete?.(finalDocId);
|
|
5585
|
+
return finalDocId;
|
|
5586
|
+
}
|
|
5587
|
+
catch (err) {
|
|
5588
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
5589
|
+
updateProgress({ status: "error", error: errorMsg });
|
|
5590
|
+
notify.error(`Resume failed: ${errorMsg}`);
|
|
5591
|
+
onError?.(errorMsg);
|
|
5592
|
+
return null;
|
|
5593
|
+
}
|
|
5594
|
+
}, [
|
|
5595
|
+
socket,
|
|
5596
|
+
connectionState,
|
|
5597
|
+
chunkSize,
|
|
5598
|
+
queryClient,
|
|
5599
|
+
notify,
|
|
5600
|
+
updateProgress,
|
|
5601
|
+
uploadChunks,
|
|
5602
|
+
onComplete,
|
|
5603
|
+
onError,
|
|
5604
|
+
]);
|
|
5605
|
+
/**
|
|
5606
|
+
* Pause the current upload
|
|
5607
|
+
*/
|
|
5608
|
+
const pause = react.useCallback(() => {
|
|
5609
|
+
if (progress.status === "uploading") {
|
|
5610
|
+
isPausedRef.current = true;
|
|
5611
|
+
updateProgress({ status: "paused" });
|
|
5612
|
+
}
|
|
5613
|
+
}, [progress.status, updateProgress]);
|
|
5614
|
+
/**
|
|
5615
|
+
* Resume a paused upload (not to be confused with resuming an interrupted upload)
|
|
5616
|
+
*/
|
|
5617
|
+
const unpause = react.useCallback(() => {
|
|
5618
|
+
if (progress.status === "paused") {
|
|
5619
|
+
isPausedRef.current = false;
|
|
5620
|
+
updateProgress({ status: "uploading" });
|
|
5621
|
+
}
|
|
5622
|
+
}, [progress.status, updateProgress]);
|
|
5623
|
+
/**
|
|
5624
|
+
* Cancel the current upload
|
|
5625
|
+
*/
|
|
5626
|
+
const cancel = react.useCallback(async () => {
|
|
5627
|
+
abortControllerRef.current?.abort();
|
|
5628
|
+
if (uploadIdRef.current) {
|
|
5629
|
+
try {
|
|
5630
|
+
await socket.librarian().abortUpload(uploadIdRef.current);
|
|
5631
|
+
}
|
|
5632
|
+
catch (err) {
|
|
5633
|
+
console.warn("Failed to abort upload on server:", err);
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
updateProgress({
|
|
5637
|
+
status: "cancelled",
|
|
5638
|
+
pendingChunks: [],
|
|
5639
|
+
});
|
|
5640
|
+
uploadIdRef.current = null;
|
|
5641
|
+
currentFileRef.current = null;
|
|
5642
|
+
}, [socket, updateProgress]);
|
|
5643
|
+
/**
|
|
5644
|
+
* Reset the upload state to idle
|
|
5645
|
+
*/
|
|
5646
|
+
const reset = react.useCallback(() => {
|
|
5647
|
+
abortControllerRef.current?.abort();
|
|
5648
|
+
uploadIdRef.current = null;
|
|
5649
|
+
currentFileRef.current = null;
|
|
5650
|
+
isPausedRef.current = false;
|
|
5651
|
+
setProgress({
|
|
5652
|
+
totalBytes: 0,
|
|
5653
|
+
bytesUploaded: 0,
|
|
5654
|
+
percentage: 0,
|
|
5655
|
+
totalChunks: 0,
|
|
5656
|
+
chunksUploaded: 0,
|
|
5657
|
+
pendingChunks: [],
|
|
5658
|
+
status: "idle",
|
|
5659
|
+
});
|
|
5660
|
+
}, []);
|
|
5661
|
+
return {
|
|
5662
|
+
// Current progress state
|
|
5663
|
+
progress,
|
|
5664
|
+
// Control methods
|
|
5665
|
+
upload,
|
|
5666
|
+
resume,
|
|
5667
|
+
pause,
|
|
5668
|
+
unpause,
|
|
5669
|
+
cancel,
|
|
5670
|
+
reset,
|
|
5671
|
+
// Convenience flags
|
|
5672
|
+
isIdle: progress.status === "idle",
|
|
5673
|
+
isUploading: progress.status === "uploading",
|
|
5674
|
+
isPaused: progress.status === "paused",
|
|
5675
|
+
isCompleted: progress.status === "completed",
|
|
5676
|
+
isError: progress.status === "error",
|
|
5677
|
+
};
|
|
5678
|
+
};
|
|
5679
|
+
|
|
5680
|
+
// Default chunk size for downloads: 1MB
|
|
5681
|
+
const DEFAULT_CHUNK_SIZE = 1024 * 1024;
|
|
5682
|
+
/**
|
|
5683
|
+
* Decode base64 string to Uint8Array
|
|
5684
|
+
*/
|
|
5685
|
+
const base64ToUint8Array = (base64) => {
|
|
5686
|
+
const binaryString = atob(base64);
|
|
5687
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
5688
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
5689
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
5690
|
+
}
|
|
5691
|
+
return bytes;
|
|
5692
|
+
};
|
|
5693
|
+
/**
|
|
5694
|
+
* Trigger browser download of a Blob
|
|
5695
|
+
*/
|
|
5696
|
+
const triggerBrowserDownload = (blob, filename) => {
|
|
5697
|
+
const url = URL.createObjectURL(blob);
|
|
5698
|
+
const link = document.createElement("a");
|
|
5699
|
+
link.href = url;
|
|
5700
|
+
link.download = filename;
|
|
5701
|
+
document.body.appendChild(link);
|
|
5702
|
+
link.click();
|
|
5703
|
+
document.body.removeChild(link);
|
|
5704
|
+
URL.revokeObjectURL(url);
|
|
5705
|
+
};
|
|
5706
|
+
/**
|
|
5707
|
+
* Hook for managing streamed document downloads with progress tracking
|
|
5708
|
+
*
|
|
5709
|
+
* Features:
|
|
5710
|
+
* - Streams large documents via WebSocket streaming response
|
|
5711
|
+
* - Progress tracking (chunks received, percentage)
|
|
5712
|
+
* - Cancel support
|
|
5713
|
+
* - Returns Blob or triggers browser download
|
|
5714
|
+
*
|
|
5715
|
+
* @param options - Configuration options for the download
|
|
5716
|
+
* @returns Download state and control methods
|
|
5717
|
+
*/
|
|
5718
|
+
const useChunkedDownload = (options = {}) => {
|
|
5719
|
+
const { chunkSize = DEFAULT_CHUNK_SIZE, onProgress, onComplete, onError, } = options;
|
|
5720
|
+
const socket = reactProvider.useSocket();
|
|
5721
|
+
const connectionState = reactProvider.useConnectionState();
|
|
5722
|
+
const notify = useNotification();
|
|
5723
|
+
// Download state
|
|
5724
|
+
const [progress, setProgress] = react.useState({
|
|
5725
|
+
totalChunks: 0,
|
|
5726
|
+
chunksReceived: 0,
|
|
5727
|
+
percentage: 0,
|
|
5728
|
+
status: "idle",
|
|
5729
|
+
});
|
|
5730
|
+
// Refs for managing download lifecycle
|
|
5731
|
+
const cancelledRef = react.useRef(false);
|
|
5732
|
+
const chunksRef = react.useRef(new Map());
|
|
5733
|
+
// Update progress and notify callback
|
|
5734
|
+
const updateProgress = react.useCallback((updates) => {
|
|
5735
|
+
setProgress((prev) => {
|
|
5736
|
+
const next = { ...prev, ...updates };
|
|
5737
|
+
onProgress?.(next);
|
|
5738
|
+
return next;
|
|
5739
|
+
});
|
|
5740
|
+
}, [onProgress]);
|
|
5741
|
+
/**
|
|
5742
|
+
* Download a document via streaming and return as Blob
|
|
5743
|
+
*/
|
|
5744
|
+
const download = react.useCallback((params) => {
|
|
5745
|
+
const { documentId, mimeType = "application/octet-stream", filename } = params;
|
|
5746
|
+
// Validate connection
|
|
5747
|
+
if (connectionState?.status !== "authenticated" &&
|
|
5748
|
+
connectionState?.status !== "unauthenticated") {
|
|
5749
|
+
const error = "Not connected to server";
|
|
5750
|
+
updateProgress({ status: "error", error });
|
|
5751
|
+
onError?.(error);
|
|
5752
|
+
return Promise.resolve(null);
|
|
5753
|
+
}
|
|
5754
|
+
// Reset state
|
|
5755
|
+
cancelledRef.current = false;
|
|
5756
|
+
chunksRef.current = new Map();
|
|
5757
|
+
updateProgress({
|
|
5758
|
+
totalChunks: 0,
|
|
5759
|
+
chunksReceived: 0,
|
|
5760
|
+
percentage: 0,
|
|
5761
|
+
status: "downloading",
|
|
5762
|
+
error: undefined,
|
|
5763
|
+
documentId,
|
|
5764
|
+
});
|
|
5765
|
+
return new Promise((resolve) => {
|
|
5766
|
+
const onChunk = (content, chunkIndex, totalChunks, complete) => {
|
|
5767
|
+
// Check for cancellation
|
|
5768
|
+
if (cancelledRef.current) {
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5771
|
+
// Store chunk
|
|
5772
|
+
const chunkData = base64ToUint8Array(content);
|
|
5773
|
+
chunksRef.current.set(chunkIndex, chunkData);
|
|
5774
|
+
const chunksReceived = chunksRef.current.size;
|
|
5775
|
+
const percentage = totalChunks > 0
|
|
5776
|
+
? Math.round((chunksReceived / totalChunks) * 100)
|
|
5777
|
+
: 0;
|
|
5778
|
+
updateProgress({
|
|
5779
|
+
totalChunks,
|
|
5780
|
+
chunksReceived,
|
|
5781
|
+
percentage,
|
|
5782
|
+
});
|
|
5783
|
+
// If complete, reassemble and return
|
|
5784
|
+
if (complete) {
|
|
5785
|
+
if (cancelledRef.current) {
|
|
5786
|
+
resolve(null);
|
|
5787
|
+
return;
|
|
5788
|
+
}
|
|
5789
|
+
// Reassemble chunks in order
|
|
5790
|
+
const orderedChunks = [];
|
|
5791
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
5792
|
+
const chunk = chunksRef.current.get(i);
|
|
5793
|
+
if (chunk) {
|
|
5794
|
+
orderedChunks.push(chunk);
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
const blob = new Blob(orderedChunks, { type: mimeType });
|
|
5798
|
+
updateProgress({
|
|
5799
|
+
status: "completed",
|
|
5800
|
+
percentage: 100,
|
|
5801
|
+
chunksReceived: totalChunks,
|
|
5802
|
+
});
|
|
5803
|
+
// Trigger browser download if filename provided
|
|
5804
|
+
if (filename) {
|
|
5805
|
+
triggerBrowserDownload(blob, filename);
|
|
5806
|
+
}
|
|
5807
|
+
notify.success("Download complete");
|
|
5808
|
+
onComplete?.(blob, documentId);
|
|
5809
|
+
resolve(blob);
|
|
5810
|
+
}
|
|
5811
|
+
};
|
|
5812
|
+
const onStreamError = (error) => {
|
|
5813
|
+
if (cancelledRef.current) {
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
updateProgress({ status: "error", error });
|
|
5817
|
+
notify.error(`Download failed: ${error}`);
|
|
5818
|
+
onError?.(error);
|
|
5819
|
+
resolve(null);
|
|
5820
|
+
};
|
|
5821
|
+
// Start streaming download
|
|
5822
|
+
socket.librarian().streamDocument(documentId, onChunk, onStreamError, chunkSize);
|
|
5823
|
+
});
|
|
5824
|
+
}, [socket, connectionState, chunkSize, notify, updateProgress, onComplete, onError]);
|
|
5825
|
+
/**
|
|
5826
|
+
* Cancel the current download
|
|
5827
|
+
*/
|
|
5828
|
+
const cancel = react.useCallback(() => {
|
|
5829
|
+
cancelledRef.current = true;
|
|
5830
|
+
chunksRef.current = new Map();
|
|
5831
|
+
updateProgress({
|
|
5832
|
+
status: "cancelled",
|
|
5833
|
+
});
|
|
5834
|
+
}, [updateProgress]);
|
|
5835
|
+
/**
|
|
5836
|
+
* Reset the download state to idle
|
|
5837
|
+
*/
|
|
5838
|
+
const reset = react.useCallback(() => {
|
|
5839
|
+
cancelledRef.current = true;
|
|
5840
|
+
chunksRef.current = new Map();
|
|
5841
|
+
setProgress({
|
|
5842
|
+
totalChunks: 0,
|
|
5843
|
+
chunksReceived: 0,
|
|
5844
|
+
percentage: 0,
|
|
5845
|
+
status: "idle",
|
|
5846
|
+
});
|
|
5847
|
+
}, []);
|
|
5848
|
+
return {
|
|
5849
|
+
// Current progress state
|
|
5850
|
+
progress,
|
|
5851
|
+
// Control methods
|
|
5852
|
+
download,
|
|
5853
|
+
cancel,
|
|
5854
|
+
reset,
|
|
5855
|
+
// Convenience flags
|
|
5856
|
+
isIdle: progress.status === "idle",
|
|
5857
|
+
isDownloading: progress.status === "downloading",
|
|
5858
|
+
isCompleted: progress.status === "completed",
|
|
5859
|
+
isError: progress.status === "error",
|
|
5860
|
+
};
|
|
5861
|
+
};
|
|
5862
|
+
|
|
5863
|
+
/**
|
|
5864
|
+
* Hook for fetching document metadata from the librarian service
|
|
5865
|
+
*/
|
|
5866
|
+
/**
|
|
5867
|
+
* Hook for fetching a single document's metadata
|
|
5868
|
+
*/
|
|
5869
|
+
const useDocumentMetadata = (options = {}) => {
|
|
5870
|
+
const { documentId, enabled } = options;
|
|
5871
|
+
const socket = reactProvider.useSocket();
|
|
5872
|
+
const connectionState = reactProvider.useConnectionState();
|
|
5873
|
+
const isSocketReady = connectionState?.status === "authenticated" ||
|
|
5874
|
+
connectionState?.status === "unauthenticated";
|
|
5875
|
+
const query = reactQuery.useQuery({
|
|
5876
|
+
queryKey: ["document-metadata", documentId],
|
|
5877
|
+
enabled: isSocketReady && !!documentId && (enabled !== false),
|
|
5878
|
+
queryFn: async () => {
|
|
5879
|
+
if (!documentId)
|
|
5880
|
+
return null;
|
|
5881
|
+
return socket.librarian().getDocumentMetadata(documentId);
|
|
5882
|
+
},
|
|
5883
|
+
});
|
|
5884
|
+
return {
|
|
5885
|
+
metadata: query.data ?? null,
|
|
5886
|
+
isLoading: query.isLoading,
|
|
5887
|
+
isError: query.isError,
|
|
5888
|
+
error: query.error,
|
|
5889
|
+
refetch: query.refetch,
|
|
5890
|
+
};
|
|
5891
|
+
};
|
|
5892
|
+
/**
|
|
5893
|
+
* Hook for fetching multiple documents' metadata
|
|
5894
|
+
*/
|
|
5895
|
+
const useDocumentsMetadata = (documentIds = []) => {
|
|
5896
|
+
const socket = reactProvider.useSocket();
|
|
5897
|
+
const connectionState = reactProvider.useConnectionState();
|
|
5898
|
+
const queryClient = reactQuery.useQueryClient();
|
|
5899
|
+
const isSocketReady = connectionState?.status === "authenticated" ||
|
|
5900
|
+
connectionState?.status === "unauthenticated";
|
|
5901
|
+
const query = reactQuery.useQuery({
|
|
5902
|
+
queryKey: ["documents-metadata", documentIds],
|
|
5903
|
+
enabled: isSocketReady && documentIds.length > 0,
|
|
5904
|
+
queryFn: async () => {
|
|
5905
|
+
const results = await Promise.all(documentIds.map(async (id) => {
|
|
5906
|
+
// Check cache first
|
|
5907
|
+
const cached = queryClient.getQueryData([
|
|
5908
|
+
"document-metadata",
|
|
5909
|
+
id,
|
|
5910
|
+
]);
|
|
5911
|
+
if (cached !== undefined) {
|
|
5912
|
+
return { id, metadata: cached };
|
|
5913
|
+
}
|
|
5914
|
+
// Fetch and cache
|
|
5915
|
+
const metadata = await socket.librarian().getDocumentMetadata(id);
|
|
5916
|
+
queryClient.setQueryData(["document-metadata", id], metadata);
|
|
5917
|
+
return { id, metadata };
|
|
5918
|
+
}));
|
|
5919
|
+
return results.reduce((acc, { id, metadata }) => {
|
|
5920
|
+
acc[id] = metadata;
|
|
5921
|
+
return acc;
|
|
5922
|
+
}, {});
|
|
5923
|
+
},
|
|
5924
|
+
});
|
|
5925
|
+
return {
|
|
5926
|
+
metadataMap: query.data ?? {},
|
|
5927
|
+
isLoading: query.isLoading,
|
|
5928
|
+
isError: query.isError,
|
|
5929
|
+
error: query.error,
|
|
5930
|
+
refetch: query.refetch,
|
|
5931
|
+
};
|
|
5932
|
+
};
|
|
5933
|
+
|
|
4645
5934
|
Object.defineProperty(exports, "ConnectionStateContext", {
|
|
4646
5935
|
enumerable: true,
|
|
4647
5936
|
get: function () { return reactProvider.ConnectionStateContext; }
|
|
@@ -4654,26 +5943,141 @@ Object.defineProperty(exports, "SocketProvider", {
|
|
|
4654
5943
|
enumerable: true,
|
|
4655
5944
|
get: function () { return reactProvider.SocketProvider; }
|
|
4656
5945
|
});
|
|
5946
|
+
Object.defineProperty(exports, "PROV", {
|
|
5947
|
+
enumerable: true,
|
|
5948
|
+
get: function () { return client.PROV; }
|
|
5949
|
+
});
|
|
5950
|
+
Object.defineProperty(exports, "PROV_ACTIVITY", {
|
|
5951
|
+
enumerable: true,
|
|
5952
|
+
get: function () { return client.PROV_ACTIVITY; }
|
|
5953
|
+
});
|
|
5954
|
+
Object.defineProperty(exports, "PROV_ENTITY", {
|
|
5955
|
+
enumerable: true,
|
|
5956
|
+
get: function () { return client.PROV_ENTITY; }
|
|
5957
|
+
});
|
|
5958
|
+
Object.defineProperty(exports, "PROV_STARTED_AT_TIME", {
|
|
5959
|
+
enumerable: true,
|
|
5960
|
+
get: function () { return client.PROV_STARTED_AT_TIME; }
|
|
5961
|
+
});
|
|
5962
|
+
Object.defineProperty(exports, "PROV_WAS_DERIVED_FROM", {
|
|
5963
|
+
enumerable: true,
|
|
5964
|
+
get: function () { return client.PROV_WAS_DERIVED_FROM; }
|
|
5965
|
+
});
|
|
5966
|
+
Object.defineProperty(exports, "PROV_WAS_GENERATED_BY", {
|
|
5967
|
+
enumerable: true,
|
|
5968
|
+
get: function () { return client.PROV_WAS_GENERATED_BY; }
|
|
5969
|
+
});
|
|
5970
|
+
Object.defineProperty(exports, "RDF", {
|
|
5971
|
+
enumerable: true,
|
|
5972
|
+
get: function () { return client.RDF; }
|
|
5973
|
+
});
|
|
5974
|
+
Object.defineProperty(exports, "RDFS", {
|
|
5975
|
+
enumerable: true,
|
|
5976
|
+
get: function () { return client.RDFS; }
|
|
5977
|
+
});
|
|
5978
|
+
Object.defineProperty(exports, "RDF_TYPE", {
|
|
5979
|
+
enumerable: true,
|
|
5980
|
+
get: function () { return client.RDF_TYPE; }
|
|
5981
|
+
});
|
|
5982
|
+
Object.defineProperty(exports, "SCHEMA", {
|
|
5983
|
+
enumerable: true,
|
|
5984
|
+
get: function () { return client.SCHEMA; }
|
|
5985
|
+
});
|
|
5986
|
+
Object.defineProperty(exports, "SCHEMA_AUTHOR", {
|
|
5987
|
+
enumerable: true,
|
|
5988
|
+
get: function () { return client.SCHEMA_AUTHOR; }
|
|
5989
|
+
});
|
|
5990
|
+
Object.defineProperty(exports, "SCHEMA_DESCRIPTION", {
|
|
5991
|
+
enumerable: true,
|
|
5992
|
+
get: function () { return client.SCHEMA_DESCRIPTION; }
|
|
5993
|
+
});
|
|
5994
|
+
Object.defineProperty(exports, "SCHEMA_KEYWORDS", {
|
|
5995
|
+
enumerable: true,
|
|
5996
|
+
get: function () { return client.SCHEMA_KEYWORDS; }
|
|
5997
|
+
});
|
|
5998
|
+
Object.defineProperty(exports, "SCHEMA_NAME", {
|
|
5999
|
+
enumerable: true,
|
|
6000
|
+
get: function () { return client.SCHEMA_NAME; }
|
|
6001
|
+
});
|
|
6002
|
+
Object.defineProperty(exports, "SKOS", {
|
|
6003
|
+
enumerable: true,
|
|
6004
|
+
get: function () { return client.SKOS; }
|
|
6005
|
+
});
|
|
6006
|
+
Object.defineProperty(exports, "SKOS_DEFINITION", {
|
|
6007
|
+
enumerable: true,
|
|
6008
|
+
get: function () { return client.SKOS_DEFINITION; }
|
|
6009
|
+
});
|
|
6010
|
+
Object.defineProperty(exports, "TG", {
|
|
6011
|
+
enumerable: true,
|
|
6012
|
+
get: function () { return client.TG; }
|
|
6013
|
+
});
|
|
6014
|
+
Object.defineProperty(exports, "TG_CONTENT", {
|
|
6015
|
+
enumerable: true,
|
|
6016
|
+
get: function () { return client.TG_CONTENT; }
|
|
6017
|
+
});
|
|
6018
|
+
Object.defineProperty(exports, "TG_DOCUMENT", {
|
|
6019
|
+
enumerable: true,
|
|
6020
|
+
get: function () { return client.TG_DOCUMENT; }
|
|
6021
|
+
});
|
|
6022
|
+
Object.defineProperty(exports, "TG_EDGE", {
|
|
6023
|
+
enumerable: true,
|
|
6024
|
+
get: function () { return client.TG_EDGE; }
|
|
6025
|
+
});
|
|
6026
|
+
Object.defineProperty(exports, "TG_EDGE_COUNT", {
|
|
6027
|
+
enumerable: true,
|
|
6028
|
+
get: function () { return client.TG_EDGE_COUNT; }
|
|
6029
|
+
});
|
|
6030
|
+
Object.defineProperty(exports, "TG_QUERY", {
|
|
6031
|
+
enumerable: true,
|
|
6032
|
+
get: function () { return client.TG_QUERY; }
|
|
6033
|
+
});
|
|
6034
|
+
Object.defineProperty(exports, "TG_REASONING", {
|
|
6035
|
+
enumerable: true,
|
|
6036
|
+
get: function () { return client.TG_REASONING; }
|
|
6037
|
+
});
|
|
6038
|
+
Object.defineProperty(exports, "TG_REIFIES", {
|
|
6039
|
+
enumerable: true,
|
|
6040
|
+
get: function () { return client.TG_REIFIES; }
|
|
6041
|
+
});
|
|
6042
|
+
Object.defineProperty(exports, "TG_SELECTED_EDGE", {
|
|
6043
|
+
enumerable: true,
|
|
6044
|
+
get: function () { return client.TG_SELECTED_EDGE; }
|
|
6045
|
+
});
|
|
4657
6046
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
4658
6047
|
exports.NotificationProvider = NotificationProvider;
|
|
4659
6048
|
exports.RDFS_LABEL = RDFS_LABEL;
|
|
4660
6049
|
exports.SETTINGS_STORAGE_KEY = SETTINGS_STORAGE_KEY;
|
|
4661
6050
|
exports.createDocId = createDocId;
|
|
6051
|
+
exports.extractQuotedTriple = extractQuotedTriple;
|
|
4662
6052
|
exports.fileToBase64 = fileToBase64;
|
|
4663
6053
|
exports.generateFlowBlueprintId = generateFlowBlueprintId;
|
|
4664
|
-
exports.
|
|
6054
|
+
exports.getEventType = getEventType;
|
|
6055
|
+
exports.getExplainTermValue = getTermValue$1;
|
|
6056
|
+
exports.getTermValue = getTermValue$3;
|
|
4665
6057
|
exports.getTriples = getTriples;
|
|
6058
|
+
exports.parseEdgeSelectionTriples = parseEdgeSelectionTriples;
|
|
6059
|
+
exports.parseExplainTriples = parseExplainTriples;
|
|
6060
|
+
exports.parseExplorationTriples = parseExplorationTriples;
|
|
6061
|
+
exports.parseFocusTriples = parseFocusTriples;
|
|
6062
|
+
exports.parseQuestionTriples = parseQuestionTriples;
|
|
6063
|
+
exports.parseSynthesisTriples = parseSynthesisTriples;
|
|
4666
6064
|
exports.prepareMetadata = prepareMetadata;
|
|
4667
6065
|
exports.textToBase64 = textToBase64;
|
|
4668
6066
|
exports.useActivity = useActivity;
|
|
4669
6067
|
exports.useAgentTools = useAgentTools;
|
|
4670
6068
|
exports.useChat = useChat;
|
|
4671
6069
|
exports.useChatSession = useChatSession;
|
|
6070
|
+
exports.useChunkedDownload = useChunkedDownload;
|
|
6071
|
+
exports.useChunkedUpload = useChunkedUpload;
|
|
4672
6072
|
exports.useCollections = useCollections;
|
|
4673
6073
|
exports.useConversation = useConversation;
|
|
4674
6074
|
exports.useDocumentEmbeddingsQuery = useDocumentEmbeddingsQuery;
|
|
6075
|
+
exports.useDocumentMetadata = useDocumentMetadata;
|
|
6076
|
+
exports.useDocumentsMetadata = useDocumentsMetadata;
|
|
4675
6077
|
exports.useEmbeddings = useEmbeddings;
|
|
4676
6078
|
exports.useEntityDetail = useEntityDetail;
|
|
6079
|
+
exports.useExplainability = useExplainability;
|
|
6080
|
+
exports.useExplainabilityStore = useExplainabilityStore;
|
|
4677
6081
|
exports.useFlowBlueprints = useFlowBlueprints;
|
|
4678
6082
|
exports.useFlowParameters = useFlowParameters;
|
|
4679
6083
|
exports.useFlows = useFlows;
|
|
@@ -4693,6 +6097,7 @@ exports.useParameterValidation = useParameterValidation;
|
|
|
4693
6097
|
exports.useProcessing = useProcessing;
|
|
4694
6098
|
exports.useProgressStateStore = useProgressStateStore;
|
|
4695
6099
|
exports.usePrompts = usePrompts;
|
|
6100
|
+
exports.useProvenance = useProvenance;
|
|
4696
6101
|
exports.useRowEmbeddingsQuery = useRowEmbeddingsQuery;
|
|
4697
6102
|
exports.useRowsQuery = useRowsQuery;
|
|
4698
6103
|
exports.useSchemas = useSchemas;
|