mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-03 09:11:59 +08:00
293 lines
7.7 KiB
TypeScript
293 lines
7.7 KiB
TypeScript
import { Authorization } from '@/constants/authorization';
|
|
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
|
import { useGetPipelineResultSearchParams } from '@/pages/dataflow-result/hooks';
|
|
import api, { restAPIv1 } from '@/utils/api';
|
|
import { getAuthorization } from '@/utils/authorization-util';
|
|
import jsPreviewExcel from '@js-preview/excel';
|
|
import { useSize } from 'ahooks';
|
|
import axios from 'axios';
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
// ZIP file header bytes "PK"
|
|
const ZIP_HEADER_0 = 0x50;
|
|
const ZIP_HEADER_1 = 0x4b;
|
|
|
|
export const isZipLikeBlob = async (blob: Blob): Promise<boolean> => {
|
|
try {
|
|
const headerSlice = blob.slice(0, 4);
|
|
const buf = await headerSlice.arrayBuffer();
|
|
const bytes = new Uint8Array(buf);
|
|
return (
|
|
bytes.length >= 2 &&
|
|
bytes[0] === ZIP_HEADER_0 &&
|
|
bytes[1] === ZIP_HEADER_1
|
|
);
|
|
} catch (e) {
|
|
console.error('Failed to inspect blob header', e);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const useDocumentResizeObserver = () => {
|
|
const [containerWidth, setContainerWidth] = useState<number>();
|
|
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
|
|
const size = useSize(containerRef);
|
|
|
|
const onResize = useCallback((width?: number) => {
|
|
if (width) {
|
|
setContainerWidth(width);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
onResize(size?.width);
|
|
}, [size?.width, onResize]);
|
|
|
|
return { containerWidth, setContainerRef };
|
|
};
|
|
|
|
function highlightPattern(text: string, pattern: string, pageNumber: number) {
|
|
if (pageNumber === 2) {
|
|
return `<mark>${text}</mark>`;
|
|
}
|
|
if (text.trim() !== '' && pattern.match(text)) {
|
|
// return pattern.replace(text, (value) => `<mark>${value}</mark>`);
|
|
return `<mark>${text}</mark>`;
|
|
}
|
|
return text.replace(pattern, (value) => `<mark>${value}</mark>`);
|
|
}
|
|
|
|
export const useHighlightText = (searchText: string = '') => {
|
|
const textRenderer = useCallback(
|
|
(textItem: any) => {
|
|
return highlightPattern(textItem.str, searchText, textItem.pageNumber);
|
|
},
|
|
[searchText],
|
|
);
|
|
|
|
return textRenderer;
|
|
};
|
|
|
|
export const useGetDocumentUrl = (isAgent: boolean) => {
|
|
const { documentId } = useGetKnowledgeSearchParams();
|
|
const { createdBy, documentId: id } = useGetPipelineResultSearchParams();
|
|
|
|
const url = useMemo(() => {
|
|
if (isAgent) {
|
|
return api.downloadFile + `?id=${id}&created_by=${createdBy}`;
|
|
}
|
|
return `${restAPIv1}/documents/${documentId}/preview`;
|
|
}, [createdBy, documentId, id, isAgent]);
|
|
|
|
return url;
|
|
};
|
|
|
|
export const useCatchError = (api: string) => {
|
|
const [error, setError] = useState('');
|
|
const fetchDocument = useCallback(async () => {
|
|
const ret = await axios.get(api);
|
|
const { data } = ret;
|
|
if (!(data instanceof ArrayBuffer) && data.code !== 0) {
|
|
setError(data.message);
|
|
}
|
|
return ret;
|
|
}, [api]);
|
|
|
|
useEffect(() => {
|
|
fetchDocument();
|
|
}, [fetchDocument]);
|
|
|
|
return { fetchDocument, error };
|
|
};
|
|
|
|
export const useFetchDocument = () => {
|
|
const fetchDocument = useCallback(async (api: string) => {
|
|
const ret = await axios.get(api, {
|
|
headers: {
|
|
[Authorization]: getAuthorization(),
|
|
},
|
|
responseType: 'arraybuffer',
|
|
});
|
|
return ret;
|
|
}, []);
|
|
|
|
return { fetchDocument };
|
|
};
|
|
|
|
export const useFetchExcel = (filePath: string) => {
|
|
const [status, setStatus] = useState(true);
|
|
const { fetchDocument } = useFetchDocument();
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const { error } = useCatchError(filePath);
|
|
|
|
const fetchDocumentAsync = useCallback(async () => {
|
|
let myExcelPreviewer;
|
|
if (containerRef.current) {
|
|
myExcelPreviewer = jsPreviewExcel.init(containerRef.current);
|
|
}
|
|
const jsonFile = await fetchDocument(filePath);
|
|
myExcelPreviewer
|
|
?.preview(jsonFile.data)
|
|
.then(() => {
|
|
setStatus(true);
|
|
})
|
|
.catch((e) => {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('failed', e);
|
|
myExcelPreviewer.destroy();
|
|
setStatus(false);
|
|
});
|
|
}, [filePath, fetchDocument]);
|
|
|
|
useEffect(() => {
|
|
fetchDocumentAsync();
|
|
}, [fetchDocumentAsync]);
|
|
|
|
return { status, containerRef, error };
|
|
};
|
|
|
|
export const useCatchDocumentError = (url: string) => {
|
|
const httpHeaders = useMemo(() => {
|
|
return {
|
|
[Authorization]: getAuthorization(),
|
|
};
|
|
}, []);
|
|
const [error, setError] = useState<string>('');
|
|
|
|
const fetchDocument = useCallback(async () => {
|
|
try {
|
|
const { data } = await axios.get(url, { headers: httpHeaders });
|
|
// Only treat as error if response is JSON with an error code
|
|
// Binary data (like PDF) won't have a code property
|
|
if (data && typeof data === 'object' && 'code' in data && data.code !== 0) {
|
|
setError(data?.message || 'Failed to load document');
|
|
}
|
|
} catch (e) {
|
|
// Network errors or non-2xx responses
|
|
const errMsg = e instanceof Error ? e.message : 'Failed to load document';
|
|
if (errMsg) {
|
|
setError(errMsg);
|
|
}
|
|
}
|
|
}, [url, httpHeaders]);
|
|
useEffect(() => {
|
|
fetchDocument();
|
|
}, [fetchDocument]);
|
|
|
|
return error;
|
|
};
|
|
|
|
const ZOOM_STEPS = [25, 50, 75, 100, 125, 150, 175, 200] as const;
|
|
|
|
const clampZoom = (scale: number, direction: 1 | -1): number => {
|
|
let idx = ZOOM_STEPS.indexOf(scale as (typeof ZOOM_STEPS)[number]);
|
|
if (idx < 0) {
|
|
if (direction > 0) {
|
|
idx = ZOOM_STEPS.findIndex((v) => v > scale);
|
|
} else {
|
|
for (let i = ZOOM_STEPS.length - 1; i >= 0; i--) {
|
|
if (ZOOM_STEPS[i] < scale) {
|
|
idx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
idx = Math.max(
|
|
0,
|
|
Math.min(ZOOM_STEPS.length - 1, idx < 0 ? 0 : idx + direction),
|
|
);
|
|
return ZOOM_STEPS[idx] ?? scale;
|
|
};
|
|
|
|
interface UseDocxPreviewZoomOptions {
|
|
url: string;
|
|
totalPages: number;
|
|
pageWidthPx?: number;
|
|
containerWidth?: number;
|
|
paddingPx?: number;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
interface UseDocxPreviewZoomResult {
|
|
zoomScale: number;
|
|
minZoom: number;
|
|
maxZoom: number;
|
|
handleZoomIn: () => void;
|
|
handleZoomOut: () => void;
|
|
resetZoom: () => void;
|
|
}
|
|
|
|
export const useDocxPreviewZoom = ({
|
|
url,
|
|
totalPages,
|
|
pageWidthPx,
|
|
containerWidth,
|
|
paddingPx = 32,
|
|
enabled = true,
|
|
}: UseDocxPreviewZoomOptions): UseDocxPreviewZoomResult => {
|
|
const [zoomScale, setZoomScale] = useState(100);
|
|
const [hasUserZoomed, setHasUserZoomed] = useState(false);
|
|
const [isInitialFitPending, setIsInitialFitPending] = useState(true);
|
|
|
|
const resetZoom = useCallback(() => {
|
|
setZoomScale(100);
|
|
setHasUserZoomed(false);
|
|
setIsInitialFitPending(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
resetZoom();
|
|
}, [url, resetZoom]);
|
|
|
|
const handleZoomIn = useCallback(() => {
|
|
setHasUserZoomed(true);
|
|
setZoomScale((s) => clampZoom(s, 1));
|
|
}, []);
|
|
|
|
const handleZoomOut = useCallback(() => {
|
|
setHasUserZoomed(true);
|
|
setZoomScale((s) => clampZoom(s, -1));
|
|
}, []);
|
|
|
|
// Fit the page width to the container on first paint and on resize,
|
|
// unless the user has manually changed the zoom level.
|
|
useEffect(() => {
|
|
if (!enabled || totalPages <= 0 || !containerWidth || !pageWidthPx) {
|
|
return;
|
|
}
|
|
|
|
const availableWidth = Math.max(0, containerWidth - paddingPx);
|
|
if (availableWidth <= 0) {
|
|
return;
|
|
}
|
|
|
|
const fitScale = Math.floor((availableWidth / pageWidthPx) * 100);
|
|
const clampedFitScale = Math.min(100, fitScale);
|
|
|
|
if (isInitialFitPending) {
|
|
setZoomScale(clampedFitScale);
|
|
setIsInitialFitPending(false);
|
|
} else if (!hasUserZoomed) {
|
|
setZoomScale(clampedFitScale);
|
|
}
|
|
}, [
|
|
enabled,
|
|
totalPages,
|
|
containerWidth,
|
|
pageWidthPx,
|
|
paddingPx,
|
|
isInitialFitPending,
|
|
hasUserZoomed,
|
|
]);
|
|
|
|
return {
|
|
zoomScale,
|
|
minZoom: ZOOM_STEPS[0],
|
|
maxZoom: ZOOM_STEPS[ZOOM_STEPS.length - 1],
|
|
handleZoomIn,
|
|
handleZoomOut,
|
|
resetZoom,
|
|
};
|
|
};
|