@trustgraph/react-state 1.5.1 → 1.5.3
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 +569 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +569 -21
- package/dist/index.esm.js.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/embeddings.d.ts +2 -2
- package/dist/state/graph-embeddings.d.ts +3 -3
- package/dist/state/graph-embeddings.d.ts.map +1 -1
- package/dist/state/inference.d.ts +2 -2
- package/dist/state/inference.d.ts.map +1 -1
- package/dist/utils/row.d.ts +1 -1
- package/dist/utils/row.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1400,9 +1400,9 @@ const useGraphSubgraph = ({ entityUri, flow, collection, }) => {
|
|
|
1400
1400
|
|
|
1401
1401
|
/**
|
|
1402
1402
|
* Custom hook for querying graph embeddings
|
|
1403
|
-
* Finds graph entities similar to the provided embedding
|
|
1403
|
+
* Finds graph entities similar to the provided embedding vector
|
|
1404
1404
|
*/
|
|
1405
|
-
const useGraphEmbeddings = ({ flow,
|
|
1405
|
+
const useGraphEmbeddings = ({ flow, vec, limit = 10, collection }) => {
|
|
1406
1406
|
const socket = reactProvider.useSocket();
|
|
1407
1407
|
const notify = useNotification();
|
|
1408
1408
|
const { settings } = useSettings();
|
|
@@ -1410,12 +1410,12 @@ const useGraphEmbeddings = ({ flow, vecs, limit = 10, collection }) => {
|
|
|
1410
1410
|
const effectiveFlow = flow ?? sessionFlowId;
|
|
1411
1411
|
const effectiveCollection = collection ?? settings.collection;
|
|
1412
1412
|
const query = reactQuery.useQuery({
|
|
1413
|
-
queryKey: ["graph-embeddings", { flow: effectiveFlow,
|
|
1414
|
-
enabled: !!
|
|
1413
|
+
queryKey: ["graph-embeddings", { flow: effectiveFlow, vec, limit, collection: effectiveCollection }],
|
|
1414
|
+
enabled: !!vec && vec.length > 0 && !!effectiveFlow,
|
|
1415
1415
|
queryFn: () => {
|
|
1416
1416
|
return socket
|
|
1417
1417
|
.flow(effectiveFlow)
|
|
1418
|
-
.graphEmbeddingsQuery(
|
|
1418
|
+
.graphEmbeddingsQuery(vec, limit, effectiveCollection)
|
|
1419
1419
|
.catch((err) => {
|
|
1420
1420
|
const message = err instanceof Error ? err.message : String(err);
|
|
1421
1421
|
notify.error(message);
|
|
@@ -1433,24 +1433,23 @@ const useGraphEmbeddings = ({ flow, vecs, limit = 10, collection }) => {
|
|
|
1433
1433
|
};
|
|
1434
1434
|
};
|
|
1435
1435
|
|
|
1436
|
-
// Take the
|
|
1436
|
+
// Take the embedding vector, and lookup entities using graph
|
|
1437
1437
|
// embeddings, add embedding to each entity row, just an easy
|
|
1438
1438
|
// place to put it
|
|
1439
1439
|
const getGraphEmbeddings = (socket, add, remove, limit, collection) => {
|
|
1440
|
-
// Take the
|
|
1441
|
-
// embeddings, add embedding to each entity row
|
|
1442
|
-
|
|
1443
|
-
return (vecs) => {
|
|
1440
|
+
// Take the embedding, and lookup entities using graph
|
|
1441
|
+
// embeddings, add embedding to each entity row
|
|
1442
|
+
return (vec) => {
|
|
1444
1443
|
const act = "Graph embedding search";
|
|
1445
1444
|
add(act);
|
|
1446
1445
|
return socket
|
|
1447
|
-
.graphEmbeddingsQuery(
|
|
1448
|
-
.then((
|
|
1446
|
+
.graphEmbeddingsQuery(vec, limit , collection)
|
|
1447
|
+
.then((matches) => {
|
|
1449
1448
|
remove(act);
|
|
1450
|
-
return
|
|
1451
|
-
.filter((
|
|
1452
|
-
.map((
|
|
1453
|
-
return { uri:
|
|
1449
|
+
return matches
|
|
1450
|
+
.filter((m) => m.entity !== null && m.entity.t === "i")
|
|
1451
|
+
.map((m) => {
|
|
1452
|
+
return { uri: m.entity.i, target: vec };
|
|
1454
1453
|
});
|
|
1455
1454
|
})
|
|
1456
1455
|
.catch((err) => {
|
|
@@ -1533,11 +1532,11 @@ const addRowEmbeddings = (socket, add, remove) => (entities) => {
|
|
|
1533
1532
|
return socket
|
|
1534
1533
|
.embeddings([text])
|
|
1535
1534
|
.then((x) => {
|
|
1536
|
-
if (x && x.length > 0
|
|
1535
|
+
if (x && x.length > 0) {
|
|
1537
1536
|
remove(act);
|
|
1538
1537
|
return {
|
|
1539
1538
|
...ent,
|
|
1540
|
-
embeddings: x[0]
|
|
1539
|
+
embeddings: x[0],
|
|
1541
1540
|
};
|
|
1542
1541
|
}
|
|
1543
1542
|
else {
|
|
@@ -1897,7 +1896,10 @@ const useChatSession = ({ flow } = {}) => {
|
|
|
1897
1896
|
// Start embeddings activity
|
|
1898
1897
|
addActivity(embActivity);
|
|
1899
1898
|
// Get labels for each entity
|
|
1900
|
-
const labelPromises = result.entities
|
|
1899
|
+
const labelPromises = result.entities
|
|
1900
|
+
.filter((match) => match.entity !== null)
|
|
1901
|
+
.map(async (match) => {
|
|
1902
|
+
const entity = match.entity;
|
|
1901
1903
|
const labelActivity = "Label " + getTermValue$2(entity);
|
|
1902
1904
|
addActivity(labelActivity);
|
|
1903
1905
|
try {
|
|
@@ -4640,6 +4642,552 @@ const useNodeDetails = (nodeId, flowId) => {
|
|
|
4640
4642
|
};
|
|
4641
4643
|
};
|
|
4642
4644
|
|
|
4645
|
+
// Default chunk size: 5MB (matches backend default)
|
|
4646
|
+
const DEFAULT_CHUNK_SIZE$1 = 5 * 1024 * 1024;
|
|
4647
|
+
// Maximum parallel chunk uploads
|
|
4648
|
+
const DEFAULT_PARALLEL_UPLOADS = 3;
|
|
4649
|
+
/**
|
|
4650
|
+
* Hook for managing chunked document uploads with progress tracking
|
|
4651
|
+
*
|
|
4652
|
+
* Features:
|
|
4653
|
+
* - Automatic chunking of large files
|
|
4654
|
+
* - Parallel chunk uploads for performance
|
|
4655
|
+
* - Progress tracking (bytes, percentage, chunks)
|
|
4656
|
+
* - Pause/resume support
|
|
4657
|
+
* - Cancel support
|
|
4658
|
+
* - Resumability after interruption
|
|
4659
|
+
*
|
|
4660
|
+
* @param options - Configuration options for the upload
|
|
4661
|
+
* @returns Upload state and control methods
|
|
4662
|
+
*/
|
|
4663
|
+
const useChunkedUpload = (options = {}) => {
|
|
4664
|
+
const { chunkSize = DEFAULT_CHUNK_SIZE$1, parallelUploads = DEFAULT_PARALLEL_UPLOADS, onProgress, onComplete, onError, } = options;
|
|
4665
|
+
const socket = reactProvider.useSocket();
|
|
4666
|
+
const connectionState = reactProvider.useConnectionState();
|
|
4667
|
+
const queryClient = reactQuery.useQueryClient();
|
|
4668
|
+
const notify = useNotification();
|
|
4669
|
+
// Upload state
|
|
4670
|
+
const [progress, setProgress] = react.useState({
|
|
4671
|
+
totalBytes: 0,
|
|
4672
|
+
bytesUploaded: 0,
|
|
4673
|
+
percentage: 0,
|
|
4674
|
+
totalChunks: 0,
|
|
4675
|
+
chunksUploaded: 0,
|
|
4676
|
+
pendingChunks: [],
|
|
4677
|
+
status: "idle",
|
|
4678
|
+
});
|
|
4679
|
+
// Refs for managing upload lifecycle
|
|
4680
|
+
const abortControllerRef = react.useRef(null);
|
|
4681
|
+
const isPausedRef = react.useRef(false);
|
|
4682
|
+
const currentFileRef = react.useRef(null);
|
|
4683
|
+
const uploadIdRef = react.useRef(null);
|
|
4684
|
+
// Update progress and notify callback
|
|
4685
|
+
const updateProgress = react.useCallback((updates) => {
|
|
4686
|
+
setProgress((prev) => {
|
|
4687
|
+
const next = { ...prev, ...updates };
|
|
4688
|
+
onProgress?.(next);
|
|
4689
|
+
return next;
|
|
4690
|
+
});
|
|
4691
|
+
}, [onProgress]);
|
|
4692
|
+
// Read a chunk from the file as base64
|
|
4693
|
+
const readChunkAsBase64 = react.useCallback(async (file, chunkIndex, chunkSz) => {
|
|
4694
|
+
const start = chunkIndex * chunkSz;
|
|
4695
|
+
const end = Math.min(start + chunkSz, file.size);
|
|
4696
|
+
const blob = file.slice(start, end);
|
|
4697
|
+
return new Promise((resolve, reject) => {
|
|
4698
|
+
const reader = new FileReader();
|
|
4699
|
+
reader.onloadend = () => {
|
|
4700
|
+
const dataUrl = reader.result;
|
|
4701
|
+
// Extract base64 from data URL
|
|
4702
|
+
const base64 = dataUrl.replace(/^data:[^;]+;base64,/, "");
|
|
4703
|
+
resolve(base64);
|
|
4704
|
+
};
|
|
4705
|
+
reader.onerror = () => reject(new Error("Failed to read file chunk"));
|
|
4706
|
+
reader.readAsDataURL(blob);
|
|
4707
|
+
});
|
|
4708
|
+
}, []);
|
|
4709
|
+
// Upload chunks in parallel with concurrency limit
|
|
4710
|
+
const uploadChunks = react.useCallback(async (file, uploadId, pendingChunks, chunkSz, totalChunks) => {
|
|
4711
|
+
const remaining = [...pendingChunks];
|
|
4712
|
+
let completedCount = totalChunks - remaining.length;
|
|
4713
|
+
let bytesUploaded = completedCount * chunkSz;
|
|
4714
|
+
// Process chunks with limited parallelism
|
|
4715
|
+
const uploadNextBatch = async () => {
|
|
4716
|
+
while (remaining.length > 0 && !abortControllerRef.current?.signal.aborted) {
|
|
4717
|
+
// Wait if paused
|
|
4718
|
+
if (isPausedRef.current) {
|
|
4719
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4720
|
+
continue;
|
|
4721
|
+
}
|
|
4722
|
+
// Take up to parallelUploads chunks
|
|
4723
|
+
const batch = remaining.splice(0, parallelUploads);
|
|
4724
|
+
// Upload batch in parallel
|
|
4725
|
+
const results = await Promise.allSettled(batch.map(async (chunkIndex) => {
|
|
4726
|
+
const content = await readChunkAsBase64(file, chunkIndex, chunkSz);
|
|
4727
|
+
await socket.librarian().uploadChunk(uploadId, chunkIndex, content);
|
|
4728
|
+
return chunkIndex;
|
|
4729
|
+
}));
|
|
4730
|
+
// Process results
|
|
4731
|
+
for (const result of results) {
|
|
4732
|
+
if (result.status === "fulfilled") {
|
|
4733
|
+
completedCount++;
|
|
4734
|
+
// Calculate actual bytes for this chunk
|
|
4735
|
+
const chunkIdx = result.value;
|
|
4736
|
+
const chunkStart = chunkIdx * chunkSz;
|
|
4737
|
+
const chunkEnd = Math.min(chunkStart + chunkSz, file.size);
|
|
4738
|
+
bytesUploaded += chunkEnd - chunkStart;
|
|
4739
|
+
updateProgress({
|
|
4740
|
+
chunksUploaded: completedCount,
|
|
4741
|
+
bytesUploaded,
|
|
4742
|
+
percentage: Math.round((bytesUploaded / file.size) * 100),
|
|
4743
|
+
pendingChunks: [...remaining],
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4746
|
+
else {
|
|
4747
|
+
// Re-add failed chunk to retry
|
|
4748
|
+
const failedIndex = batch[results.indexOf(result)];
|
|
4749
|
+
remaining.push(failedIndex);
|
|
4750
|
+
console.warn(`Chunk ${failedIndex} failed, will retry:`, result.reason);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
};
|
|
4755
|
+
await uploadNextBatch();
|
|
4756
|
+
}, [socket, parallelUploads, readChunkAsBase64, updateProgress]);
|
|
4757
|
+
/**
|
|
4758
|
+
* Start a new chunked upload
|
|
4759
|
+
*/
|
|
4760
|
+
const upload = react.useCallback(async (params) => {
|
|
4761
|
+
const { file, title, comments = "", tags = [], collection, documentId } = params;
|
|
4762
|
+
// Validate connection
|
|
4763
|
+
if (connectionState?.status !== "authenticated" &&
|
|
4764
|
+
connectionState?.status !== "unauthenticated") {
|
|
4765
|
+
const error = "Not connected to server";
|
|
4766
|
+
updateProgress({ status: "error", error });
|
|
4767
|
+
onError?.(error);
|
|
4768
|
+
return null;
|
|
4769
|
+
}
|
|
4770
|
+
// Reset state
|
|
4771
|
+
abortControllerRef.current = new AbortController();
|
|
4772
|
+
isPausedRef.current = false;
|
|
4773
|
+
currentFileRef.current = file;
|
|
4774
|
+
const docId = documentId || createDocId();
|
|
4775
|
+
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
4776
|
+
const pendingChunks = Array.from({ length: totalChunks }, (_, i) => i);
|
|
4777
|
+
updateProgress({
|
|
4778
|
+
totalBytes: file.size,
|
|
4779
|
+
bytesUploaded: 0,
|
|
4780
|
+
percentage: 0,
|
|
4781
|
+
totalChunks,
|
|
4782
|
+
chunksUploaded: 0,
|
|
4783
|
+
pendingChunks,
|
|
4784
|
+
status: "preparing",
|
|
4785
|
+
error: undefined,
|
|
4786
|
+
uploadId: undefined,
|
|
4787
|
+
documentId: undefined,
|
|
4788
|
+
});
|
|
4789
|
+
try {
|
|
4790
|
+
// Initialize upload session
|
|
4791
|
+
const metadata = {
|
|
4792
|
+
id: docId,
|
|
4793
|
+
time: Math.floor(Date.now() / 1000),
|
|
4794
|
+
kind: file.type || "application/octet-stream",
|
|
4795
|
+
title,
|
|
4796
|
+
comments,
|
|
4797
|
+
user: "trustgraph", // Will be set by server based on auth
|
|
4798
|
+
collection: collection || "default",
|
|
4799
|
+
tags,
|
|
4800
|
+
};
|
|
4801
|
+
const beginResponse = await socket
|
|
4802
|
+
.librarian()
|
|
4803
|
+
.beginUpload(metadata, file.size, chunkSize);
|
|
4804
|
+
const uploadId = beginResponse["upload-id"];
|
|
4805
|
+
uploadIdRef.current = uploadId;
|
|
4806
|
+
updateProgress({
|
|
4807
|
+
status: "uploading",
|
|
4808
|
+
uploadId,
|
|
4809
|
+
});
|
|
4810
|
+
// Upload all chunks
|
|
4811
|
+
await uploadChunks(file, uploadId, pendingChunks, chunkSize, totalChunks);
|
|
4812
|
+
// Check if cancelled
|
|
4813
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
4814
|
+
return null;
|
|
4815
|
+
}
|
|
4816
|
+
// Complete the upload
|
|
4817
|
+
updateProgress({ status: "completing" });
|
|
4818
|
+
const completeResponse = await socket.librarian().completeUpload(uploadId);
|
|
4819
|
+
const finalDocId = completeResponse["document-id"];
|
|
4820
|
+
updateProgress({
|
|
4821
|
+
status: "completed",
|
|
4822
|
+
documentId: finalDocId,
|
|
4823
|
+
percentage: 100,
|
|
4824
|
+
});
|
|
4825
|
+
// Invalidate documents cache
|
|
4826
|
+
queryClient.invalidateQueries({ queryKey: ["documents"] });
|
|
4827
|
+
notify.success(`Upload complete: ${title}`);
|
|
4828
|
+
onComplete?.(finalDocId);
|
|
4829
|
+
return finalDocId;
|
|
4830
|
+
}
|
|
4831
|
+
catch (err) {
|
|
4832
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
4833
|
+
updateProgress({ status: "error", error: errorMsg });
|
|
4834
|
+
notify.error(`Upload failed: ${errorMsg}`);
|
|
4835
|
+
onError?.(errorMsg);
|
|
4836
|
+
return null;
|
|
4837
|
+
}
|
|
4838
|
+
}, [
|
|
4839
|
+
socket,
|
|
4840
|
+
connectionState,
|
|
4841
|
+
chunkSize,
|
|
4842
|
+
queryClient,
|
|
4843
|
+
notify,
|
|
4844
|
+
updateProgress,
|
|
4845
|
+
uploadChunks,
|
|
4846
|
+
onComplete,
|
|
4847
|
+
onError,
|
|
4848
|
+
]);
|
|
4849
|
+
/**
|
|
4850
|
+
* Resume an interrupted upload
|
|
4851
|
+
*/
|
|
4852
|
+
const resume = react.useCallback(async (params) => {
|
|
4853
|
+
const { uploadId, file } = params;
|
|
4854
|
+
// Validate connection
|
|
4855
|
+
if (connectionState?.status !== "authenticated" &&
|
|
4856
|
+
connectionState?.status !== "unauthenticated") {
|
|
4857
|
+
const error = "Not connected to server";
|
|
4858
|
+
updateProgress({ status: "error", error });
|
|
4859
|
+
onError?.(error);
|
|
4860
|
+
return null;
|
|
4861
|
+
}
|
|
4862
|
+
abortControllerRef.current = new AbortController();
|
|
4863
|
+
isPausedRef.current = false;
|
|
4864
|
+
currentFileRef.current = file;
|
|
4865
|
+
uploadIdRef.current = uploadId;
|
|
4866
|
+
updateProgress({
|
|
4867
|
+
status: "preparing",
|
|
4868
|
+
uploadId,
|
|
4869
|
+
});
|
|
4870
|
+
try {
|
|
4871
|
+
// Get current upload status
|
|
4872
|
+
const status = await socket.librarian().getUploadStatus(uploadId);
|
|
4873
|
+
if (status["upload-state"] === "completed") {
|
|
4874
|
+
updateProgress({ status: "completed" });
|
|
4875
|
+
return null;
|
|
4876
|
+
}
|
|
4877
|
+
if (status["upload-state"] === "expired") {
|
|
4878
|
+
throw new Error("Upload session has expired");
|
|
4879
|
+
}
|
|
4880
|
+
const totalChunks = status["total-chunks"];
|
|
4881
|
+
const missingChunks = status["missing-chunks"];
|
|
4882
|
+
const bytesReceived = status["bytes-received"];
|
|
4883
|
+
const totalBytes = status["total-bytes"];
|
|
4884
|
+
updateProgress({
|
|
4885
|
+
totalBytes,
|
|
4886
|
+
bytesUploaded: bytesReceived,
|
|
4887
|
+
percentage: Math.round((bytesReceived / totalBytes) * 100),
|
|
4888
|
+
totalChunks,
|
|
4889
|
+
chunksUploaded: totalChunks - missingChunks.length,
|
|
4890
|
+
pendingChunks: missingChunks,
|
|
4891
|
+
status: "uploading",
|
|
4892
|
+
});
|
|
4893
|
+
// Upload missing chunks
|
|
4894
|
+
const effectiveChunkSize = status["chunk-size"] || chunkSize;
|
|
4895
|
+
await uploadChunks(file, uploadId, missingChunks, effectiveChunkSize, totalChunks);
|
|
4896
|
+
// Check if cancelled
|
|
4897
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
4898
|
+
return null;
|
|
4899
|
+
}
|
|
4900
|
+
// Complete the upload
|
|
4901
|
+
updateProgress({ status: "completing" });
|
|
4902
|
+
const completeResponse = await socket.librarian().completeUpload(uploadId);
|
|
4903
|
+
const finalDocId = completeResponse["document-id"];
|
|
4904
|
+
updateProgress({
|
|
4905
|
+
status: "completed",
|
|
4906
|
+
documentId: finalDocId,
|
|
4907
|
+
percentage: 100,
|
|
4908
|
+
});
|
|
4909
|
+
// Invalidate documents cache
|
|
4910
|
+
queryClient.invalidateQueries({ queryKey: ["documents"] });
|
|
4911
|
+
notify.success("Upload resumed and completed");
|
|
4912
|
+
onComplete?.(finalDocId);
|
|
4913
|
+
return finalDocId;
|
|
4914
|
+
}
|
|
4915
|
+
catch (err) {
|
|
4916
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
4917
|
+
updateProgress({ status: "error", error: errorMsg });
|
|
4918
|
+
notify.error(`Resume failed: ${errorMsg}`);
|
|
4919
|
+
onError?.(errorMsg);
|
|
4920
|
+
return null;
|
|
4921
|
+
}
|
|
4922
|
+
}, [
|
|
4923
|
+
socket,
|
|
4924
|
+
connectionState,
|
|
4925
|
+
chunkSize,
|
|
4926
|
+
queryClient,
|
|
4927
|
+
notify,
|
|
4928
|
+
updateProgress,
|
|
4929
|
+
uploadChunks,
|
|
4930
|
+
onComplete,
|
|
4931
|
+
onError,
|
|
4932
|
+
]);
|
|
4933
|
+
/**
|
|
4934
|
+
* Pause the current upload
|
|
4935
|
+
*/
|
|
4936
|
+
const pause = react.useCallback(() => {
|
|
4937
|
+
if (progress.status === "uploading") {
|
|
4938
|
+
isPausedRef.current = true;
|
|
4939
|
+
updateProgress({ status: "paused" });
|
|
4940
|
+
}
|
|
4941
|
+
}, [progress.status, updateProgress]);
|
|
4942
|
+
/**
|
|
4943
|
+
* Resume a paused upload (not to be confused with resuming an interrupted upload)
|
|
4944
|
+
*/
|
|
4945
|
+
const unpause = react.useCallback(() => {
|
|
4946
|
+
if (progress.status === "paused") {
|
|
4947
|
+
isPausedRef.current = false;
|
|
4948
|
+
updateProgress({ status: "uploading" });
|
|
4949
|
+
}
|
|
4950
|
+
}, [progress.status, updateProgress]);
|
|
4951
|
+
/**
|
|
4952
|
+
* Cancel the current upload
|
|
4953
|
+
*/
|
|
4954
|
+
const cancel = react.useCallback(async () => {
|
|
4955
|
+
abortControllerRef.current?.abort();
|
|
4956
|
+
if (uploadIdRef.current) {
|
|
4957
|
+
try {
|
|
4958
|
+
await socket.librarian().abortUpload(uploadIdRef.current);
|
|
4959
|
+
}
|
|
4960
|
+
catch (err) {
|
|
4961
|
+
console.warn("Failed to abort upload on server:", err);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
updateProgress({
|
|
4965
|
+
status: "cancelled",
|
|
4966
|
+
pendingChunks: [],
|
|
4967
|
+
});
|
|
4968
|
+
uploadIdRef.current = null;
|
|
4969
|
+
currentFileRef.current = null;
|
|
4970
|
+
}, [socket, updateProgress]);
|
|
4971
|
+
/**
|
|
4972
|
+
* Reset the upload state to idle
|
|
4973
|
+
*/
|
|
4974
|
+
const reset = react.useCallback(() => {
|
|
4975
|
+
abortControllerRef.current?.abort();
|
|
4976
|
+
uploadIdRef.current = null;
|
|
4977
|
+
currentFileRef.current = null;
|
|
4978
|
+
isPausedRef.current = false;
|
|
4979
|
+
setProgress({
|
|
4980
|
+
totalBytes: 0,
|
|
4981
|
+
bytesUploaded: 0,
|
|
4982
|
+
percentage: 0,
|
|
4983
|
+
totalChunks: 0,
|
|
4984
|
+
chunksUploaded: 0,
|
|
4985
|
+
pendingChunks: [],
|
|
4986
|
+
status: "idle",
|
|
4987
|
+
});
|
|
4988
|
+
}, []);
|
|
4989
|
+
return {
|
|
4990
|
+
// Current progress state
|
|
4991
|
+
progress,
|
|
4992
|
+
// Control methods
|
|
4993
|
+
upload,
|
|
4994
|
+
resume,
|
|
4995
|
+
pause,
|
|
4996
|
+
unpause,
|
|
4997
|
+
cancel,
|
|
4998
|
+
reset,
|
|
4999
|
+
// Convenience flags
|
|
5000
|
+
isIdle: progress.status === "idle",
|
|
5001
|
+
isUploading: progress.status === "uploading",
|
|
5002
|
+
isPaused: progress.status === "paused",
|
|
5003
|
+
isCompleted: progress.status === "completed",
|
|
5004
|
+
isError: progress.status === "error",
|
|
5005
|
+
};
|
|
5006
|
+
};
|
|
5007
|
+
|
|
5008
|
+
// Default chunk size for downloads: 1MB
|
|
5009
|
+
const DEFAULT_CHUNK_SIZE = 1024 * 1024;
|
|
5010
|
+
/**
|
|
5011
|
+
* Decode base64 string to Uint8Array
|
|
5012
|
+
*/
|
|
5013
|
+
const base64ToUint8Array = (base64) => {
|
|
5014
|
+
const binaryString = atob(base64);
|
|
5015
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
5016
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
5017
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
5018
|
+
}
|
|
5019
|
+
return bytes;
|
|
5020
|
+
};
|
|
5021
|
+
/**
|
|
5022
|
+
* Trigger browser download of a Blob
|
|
5023
|
+
*/
|
|
5024
|
+
const triggerBrowserDownload = (blob, filename) => {
|
|
5025
|
+
const url = URL.createObjectURL(blob);
|
|
5026
|
+
const link = document.createElement("a");
|
|
5027
|
+
link.href = url;
|
|
5028
|
+
link.download = filename;
|
|
5029
|
+
document.body.appendChild(link);
|
|
5030
|
+
link.click();
|
|
5031
|
+
document.body.removeChild(link);
|
|
5032
|
+
URL.revokeObjectURL(url);
|
|
5033
|
+
};
|
|
5034
|
+
/**
|
|
5035
|
+
* Hook for managing streamed document downloads with progress tracking
|
|
5036
|
+
*
|
|
5037
|
+
* Features:
|
|
5038
|
+
* - Streams large documents via WebSocket streaming response
|
|
5039
|
+
* - Progress tracking (chunks received, percentage)
|
|
5040
|
+
* - Cancel support
|
|
5041
|
+
* - Returns Blob or triggers browser download
|
|
5042
|
+
*
|
|
5043
|
+
* @param options - Configuration options for the download
|
|
5044
|
+
* @returns Download state and control methods
|
|
5045
|
+
*/
|
|
5046
|
+
const useChunkedDownload = (options = {}) => {
|
|
5047
|
+
const { chunkSize = DEFAULT_CHUNK_SIZE, onProgress, onComplete, onError, } = options;
|
|
5048
|
+
const socket = reactProvider.useSocket();
|
|
5049
|
+
const connectionState = reactProvider.useConnectionState();
|
|
5050
|
+
const notify = useNotification();
|
|
5051
|
+
// Download state
|
|
5052
|
+
const [progress, setProgress] = react.useState({
|
|
5053
|
+
totalChunks: 0,
|
|
5054
|
+
chunksReceived: 0,
|
|
5055
|
+
percentage: 0,
|
|
5056
|
+
status: "idle",
|
|
5057
|
+
});
|
|
5058
|
+
// Refs for managing download lifecycle
|
|
5059
|
+
const cancelledRef = react.useRef(false);
|
|
5060
|
+
const chunksRef = react.useRef(new Map());
|
|
5061
|
+
// Update progress and notify callback
|
|
5062
|
+
const updateProgress = react.useCallback((updates) => {
|
|
5063
|
+
setProgress((prev) => {
|
|
5064
|
+
const next = { ...prev, ...updates };
|
|
5065
|
+
onProgress?.(next);
|
|
5066
|
+
return next;
|
|
5067
|
+
});
|
|
5068
|
+
}, [onProgress]);
|
|
5069
|
+
/**
|
|
5070
|
+
* Download a document via streaming and return as Blob
|
|
5071
|
+
*/
|
|
5072
|
+
const download = react.useCallback((params) => {
|
|
5073
|
+
const { documentId, mimeType = "application/octet-stream", filename } = params;
|
|
5074
|
+
// Validate connection
|
|
5075
|
+
if (connectionState?.status !== "authenticated" &&
|
|
5076
|
+
connectionState?.status !== "unauthenticated") {
|
|
5077
|
+
const error = "Not connected to server";
|
|
5078
|
+
updateProgress({ status: "error", error });
|
|
5079
|
+
onError?.(error);
|
|
5080
|
+
return Promise.resolve(null);
|
|
5081
|
+
}
|
|
5082
|
+
// Reset state
|
|
5083
|
+
cancelledRef.current = false;
|
|
5084
|
+
chunksRef.current = new Map();
|
|
5085
|
+
updateProgress({
|
|
5086
|
+
totalChunks: 0,
|
|
5087
|
+
chunksReceived: 0,
|
|
5088
|
+
percentage: 0,
|
|
5089
|
+
status: "downloading",
|
|
5090
|
+
error: undefined,
|
|
5091
|
+
documentId,
|
|
5092
|
+
});
|
|
5093
|
+
return new Promise((resolve) => {
|
|
5094
|
+
const onChunk = (content, chunkIndex, totalChunks, complete) => {
|
|
5095
|
+
// Check for cancellation
|
|
5096
|
+
if (cancelledRef.current) {
|
|
5097
|
+
return;
|
|
5098
|
+
}
|
|
5099
|
+
// Store chunk
|
|
5100
|
+
const chunkData = base64ToUint8Array(content);
|
|
5101
|
+
chunksRef.current.set(chunkIndex, chunkData);
|
|
5102
|
+
const chunksReceived = chunksRef.current.size;
|
|
5103
|
+
const percentage = totalChunks > 0
|
|
5104
|
+
? Math.round((chunksReceived / totalChunks) * 100)
|
|
5105
|
+
: 0;
|
|
5106
|
+
updateProgress({
|
|
5107
|
+
totalChunks,
|
|
5108
|
+
chunksReceived,
|
|
5109
|
+
percentage,
|
|
5110
|
+
});
|
|
5111
|
+
// If complete, reassemble and return
|
|
5112
|
+
if (complete) {
|
|
5113
|
+
if (cancelledRef.current) {
|
|
5114
|
+
resolve(null);
|
|
5115
|
+
return;
|
|
5116
|
+
}
|
|
5117
|
+
// Reassemble chunks in order
|
|
5118
|
+
const orderedChunks = [];
|
|
5119
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
5120
|
+
const chunk = chunksRef.current.get(i);
|
|
5121
|
+
if (chunk) {
|
|
5122
|
+
orderedChunks.push(chunk);
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
const blob = new Blob(orderedChunks, { type: mimeType });
|
|
5126
|
+
updateProgress({
|
|
5127
|
+
status: "completed",
|
|
5128
|
+
percentage: 100,
|
|
5129
|
+
chunksReceived: totalChunks,
|
|
5130
|
+
});
|
|
5131
|
+
// Trigger browser download if filename provided
|
|
5132
|
+
if (filename) {
|
|
5133
|
+
triggerBrowserDownload(blob, filename);
|
|
5134
|
+
}
|
|
5135
|
+
notify.success("Download complete");
|
|
5136
|
+
onComplete?.(blob, documentId);
|
|
5137
|
+
resolve(blob);
|
|
5138
|
+
}
|
|
5139
|
+
};
|
|
5140
|
+
const onStreamError = (error) => {
|
|
5141
|
+
if (cancelledRef.current) {
|
|
5142
|
+
return;
|
|
5143
|
+
}
|
|
5144
|
+
updateProgress({ status: "error", error });
|
|
5145
|
+
notify.error(`Download failed: ${error}`);
|
|
5146
|
+
onError?.(error);
|
|
5147
|
+
resolve(null);
|
|
5148
|
+
};
|
|
5149
|
+
// Start streaming download
|
|
5150
|
+
socket.librarian().streamDocument(documentId, onChunk, onStreamError, chunkSize);
|
|
5151
|
+
});
|
|
5152
|
+
}, [socket, connectionState, chunkSize, notify, updateProgress, onComplete, onError]);
|
|
5153
|
+
/**
|
|
5154
|
+
* Cancel the current download
|
|
5155
|
+
*/
|
|
5156
|
+
const cancel = react.useCallback(() => {
|
|
5157
|
+
cancelledRef.current = true;
|
|
5158
|
+
chunksRef.current = new Map();
|
|
5159
|
+
updateProgress({
|
|
5160
|
+
status: "cancelled",
|
|
5161
|
+
});
|
|
5162
|
+
}, [updateProgress]);
|
|
5163
|
+
/**
|
|
5164
|
+
* Reset the download state to idle
|
|
5165
|
+
*/
|
|
5166
|
+
const reset = react.useCallback(() => {
|
|
5167
|
+
cancelledRef.current = true;
|
|
5168
|
+
chunksRef.current = new Map();
|
|
5169
|
+
setProgress({
|
|
5170
|
+
totalChunks: 0,
|
|
5171
|
+
chunksReceived: 0,
|
|
5172
|
+
percentage: 0,
|
|
5173
|
+
status: "idle",
|
|
5174
|
+
});
|
|
5175
|
+
}, []);
|
|
5176
|
+
return {
|
|
5177
|
+
// Current progress state
|
|
5178
|
+
progress,
|
|
5179
|
+
// Control methods
|
|
5180
|
+
download,
|
|
5181
|
+
cancel,
|
|
5182
|
+
reset,
|
|
5183
|
+
// Convenience flags
|
|
5184
|
+
isIdle: progress.status === "idle",
|
|
5185
|
+
isDownloading: progress.status === "downloading",
|
|
5186
|
+
isCompleted: progress.status === "completed",
|
|
5187
|
+
isError: progress.status === "error",
|
|
5188
|
+
};
|
|
5189
|
+
};
|
|
5190
|
+
|
|
4643
5191
|
Object.defineProperty(exports, "ConnectionStateContext", {
|
|
4644
5192
|
enumerable: true,
|
|
4645
5193
|
get: function () { return reactProvider.ConnectionStateContext; }
|
|
@@ -4667,6 +5215,8 @@ exports.useActivity = useActivity;
|
|
|
4667
5215
|
exports.useAgentTools = useAgentTools;
|
|
4668
5216
|
exports.useChat = useChat;
|
|
4669
5217
|
exports.useChatSession = useChatSession;
|
|
5218
|
+
exports.useChunkedDownload = useChunkedDownload;
|
|
5219
|
+
exports.useChunkedUpload = useChunkedUpload;
|
|
4670
5220
|
exports.useCollections = useCollections;
|
|
4671
5221
|
exports.useConversation = useConversation;
|
|
4672
5222
|
exports.useDocumentEmbeddingsQuery = useDocumentEmbeddingsQuery;
|