@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 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$2 = (term) => {
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$2(triples[0].o);
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$2(t.s), add, remove, collection).then((label) => {
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$2(t.p), add, remove, collection).then((label) => {
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$2(t.o),
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$1 = (term) => {
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$1(t.s);
1223
+ const sourceId = getTermValue$2(t.s);
1220
1224
  // Target is always an entity now (we filtered out literals above)
1221
- const targetId = getTermValue$1(t.o);
1225
+ const targetId = getTermValue$2(t.o);
1222
1226
  // Links have an ID so that this edge is unique
1223
- const linkId = getTermValue$1(t.s) + "@@" + getTermValue$1(t.p) + "@@" + getTermValue$1(t.o);
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$2(entity);
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$2(resp[0].o),
1923
- uri: getTermValue$2(resp[0].s),
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.getTermValue = getTermValue$2;
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;