mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 21:02:40 +02:00
Merge pull request #529 from CREDO23/feat/add-jotai-tanstack
[Feat] Documents | Add Jotai & Tanstack
This commit is contained in:
commit
194292a641
21 changed files with 948 additions and 688 deletions
|
|
@ -3,7 +3,9 @@
|
|||
import { ChatInput } from "@llamaindex/chat-ui";
|
||||
import { Brain, Check, FolderOpen, Minus, Plus, PlusCircle, Zap } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import React, { Suspense, useCallback, useState } from "react";
|
||||
import React, { Suspense, useCallback, useState, useMemo } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
||||
import { DocumentsDataTable } from "@/components/chat/DocumentsDataTable";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -25,7 +27,6 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import { useDocumentTypes } from "@/hooks/use-document-types";
|
||||
import type { Document } from "@/hooks/use-documents";
|
||||
import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
|
||||
import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
|
||||
|
|
@ -118,11 +119,20 @@ const ConnectorSelector = React.memo(
|
|||
const router = useRouter();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Fetch immediately (not lazy) so the button can show the correct count
|
||||
const { documentTypes, isLoading, isLoaded, fetchDocumentTypes } = useDocumentTypes(
|
||||
Number(search_space_id),
|
||||
false
|
||||
);
|
||||
// Use the documentTypeCountsAtom for fetching document types
|
||||
const [documentTypeCountsQuery] = useAtom(documentTypeCountsAtom);
|
||||
const { data: documentTypeCountsData, isLoading, refetch: fetchDocumentTypes } = documentTypeCountsQuery;
|
||||
|
||||
// Transform the response into the expected format
|
||||
const documentTypes = useMemo(() => {
|
||||
if (!documentTypeCountsData) return [];
|
||||
return Object.entries(documentTypeCountsData).map(([type, count]) => ({
|
||||
type,
|
||||
count,
|
||||
}));
|
||||
}, [documentTypeCountsData]);
|
||||
|
||||
const isLoaded = !!documentTypeCountsData;
|
||||
|
||||
// Fetch live search connectors immediately (non-indexable)
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { ArrowUpDown, Calendar, FileText, Filter, Plus, Search } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -31,7 +32,11 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import { type Document, type DocumentType, useDocuments } from "@/hooks/use-documents";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { Document, DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
||||
|
||||
interface DocumentsDataTableProps {
|
||||
searchSpaceId: number;
|
||||
|
|
@ -104,8 +109,8 @@ const columns: ColumnDef<Document>[] = [
|
|||
cell: ({ row }) => {
|
||||
const type = row.getValue("document_type") as DocumentType;
|
||||
return (
|
||||
<div className="flex items-center gap-2" title={type}>
|
||||
<span className="text-primary">{getConnectorIcon(type)}</span>
|
||||
<div className="flex items-center gap-2" title={String(type)}>
|
||||
<span className="text-primary">{getConnectorIcon(String(type))}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
@ -182,52 +187,59 @@ export function DocumentsDataTable({
|
|||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebounced(search, 300);
|
||||
const [documentTypeFilter, setDocumentTypeFilter] = useState<string[]>([]);
|
||||
const [documentTypeFilter, setDocumentTypeFilter] = useState<DocumentTypeEnum[]>([]);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [typeCounts, setTypeCounts] = useState<Record<string, number>>({});
|
||||
const {data : typeCounts } = useAtomValue(documentTypeCountsAtom);
|
||||
|
||||
// Use server-side pagination, search, and filtering
|
||||
const { documents, total, loading, fetchDocuments, searchDocuments, getDocumentTypeCounts } =
|
||||
useDocuments(searchSpaceId, {
|
||||
page: pageIndex,
|
||||
pageSize: pageSize,
|
||||
});
|
||||
const fetchQueryParams = useMemo(
|
||||
() => ({
|
||||
search_space_id: searchSpaceId,
|
||||
page: pageIndex ,
|
||||
page_size: pageSize,
|
||||
...(documentTypeFilter.length > 0 && { document_types: documentTypeFilter }),
|
||||
}),
|
||||
[searchSpaceId, pageIndex, pageSize, documentTypeFilter, debouncedSearch]
|
||||
);
|
||||
|
||||
// Fetch document type counts on mount
|
||||
useEffect(() => {
|
||||
if (searchSpaceId && getDocumentTypeCounts) {
|
||||
getDocumentTypeCounts().then(setTypeCounts);
|
||||
const searchQueryParams = useMemo(() => {
|
||||
return {
|
||||
search_space_id: searchSpaceId,
|
||||
page: pageIndex ,
|
||||
page_size: pageSize,
|
||||
...(documentTypeFilter.length > 0 && { document_types: documentTypeFilter }),
|
||||
title : debouncedSearch,
|
||||
}
|
||||
}, [searchSpaceId, getDocumentTypeCounts]);
|
||||
},[debouncedSearch, searchSpaceId, pageIndex, pageSize, documentTypeFilter, debouncedSearch])
|
||||
|
||||
// Refetch when pagination changes or when search/filters change
|
||||
useEffect(() => {
|
||||
if (searchSpaceId) {
|
||||
if (debouncedSearch.trim()) {
|
||||
searchDocuments?.(
|
||||
debouncedSearch,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
documentTypeFilter.length > 0 ? documentTypeFilter : undefined
|
||||
);
|
||||
} else {
|
||||
fetchDocuments?.(
|
||||
pageIndex,
|
||||
pageSize,
|
||||
documentTypeFilter.length > 0 ? documentTypeFilter : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
pageIndex,
|
||||
pageSize,
|
||||
debouncedSearch,
|
||||
documentTypeFilter,
|
||||
searchSpaceId,
|
||||
fetchDocuments,
|
||||
searchDocuments,
|
||||
]);
|
||||
// Use query for fetching documents
|
||||
const {
|
||||
data: documents,
|
||||
isLoading: isDocumentsLoading,
|
||||
} = useQuery({
|
||||
queryKey: cacheKeys.documents.withQueryParams(fetchQueryParams),
|
||||
queryFn: () => documentsApiService.getDocuments({ queryParams : fetchQueryParams }),
|
||||
staleTime: 3 * 60 * 1000, // 3 minutes
|
||||
enabled: !!searchSpaceId && !debouncedSearch.trim(),
|
||||
});
|
||||
|
||||
// Seaching
|
||||
const {
|
||||
data: searchedDocuments,
|
||||
isLoading: isSearchedDocumentsLoading,
|
||||
} = useQuery({
|
||||
queryKey: cacheKeys.documents.withQueryParams(searchQueryParams),
|
||||
queryFn: () => documentsApiService.searchDocuments({ queryParams : searchQueryParams }),
|
||||
staleTime: 3 * 60 * 1000, // 3 minutes
|
||||
enabled: !!searchSpaceId && !!debouncedSearch.trim(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Use query data when not searching, otherwise use hook data
|
||||
const actualDocuments = debouncedSearch.trim() ? searchedDocuments?.items|| [] : documents?.items|| [];
|
||||
const actualTotal = debouncedSearch.trim() ? searchedDocuments?.total || 0 : documents?.total || 0;
|
||||
const actualLoading = debouncedSearch.trim() ? isSearchedDocumentsLoading : isDocumentsLoading;
|
||||
|
||||
// Memoize initial row selection to prevent infinite loops
|
||||
const initialRowSelection = useMemo(() => {
|
||||
|
|
@ -272,14 +284,14 @@ export function DocumentsDataTable({
|
|||
|
||||
// Update the selected documents map when row selection changes
|
||||
useEffect(() => {
|
||||
if (!documents || documents.length === 0) return;
|
||||
if (!actualDocuments || actualDocuments.length === 0) return;
|
||||
|
||||
setSelectedDocumentsMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
let hasChanges = false;
|
||||
|
||||
// Process only current page documents
|
||||
for (const doc of documents) {
|
||||
for (const doc of actualDocuments) {
|
||||
const docId = doc.id;
|
||||
const isSelected = rowSelection[docId.toString()];
|
||||
const wasInMap = newMap.has(docId);
|
||||
|
|
@ -319,14 +331,14 @@ export function DocumentsDataTable({
|
|||
}, [selectedDocumentsArray, onSelectionChange]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: documents || [],
|
||||
data: actualDocuments || [],
|
||||
columns,
|
||||
getRowId: (row) => row.id.toString(),
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
manualPagination: true,
|
||||
pageCount: Math.ceil(total / pageSize),
|
||||
pageCount: Math.ceil(actualTotal / pageSize),
|
||||
state: { sorting, rowSelection, pagination: { pageIndex, pageSize } },
|
||||
});
|
||||
|
||||
|
|
@ -344,7 +356,7 @@ export function DocumentsDataTable({
|
|||
setRowSelection(newSelection);
|
||||
}, [table, rowSelection]);
|
||||
|
||||
const handleToggleType = useCallback((type: string, checked: boolean) => {
|
||||
const handleToggleType = useCallback((type: DocumentTypeEnum, checked: boolean) => {
|
||||
setDocumentTypeFilter((prev) => {
|
||||
if (checked) {
|
||||
return [...prev, type];
|
||||
|
|
@ -358,7 +370,7 @@ export function DocumentsDataTable({
|
|||
|
||||
// Get available document types from type counts (memoized)
|
||||
const availableTypes = useMemo(() => {
|
||||
const types = Object.keys(typeCounts);
|
||||
const types = typeCounts ? Object.keys(typeCounts) as DocumentTypeEnum[] : [];
|
||||
return types.length > 0 ? types.sort() : [];
|
||||
}, [typeCounts]);
|
||||
|
||||
|
|
@ -408,7 +420,7 @@ export function DocumentsDataTable({
|
|||
className="flex grow justify-between gap-2 font-normal text-sm cursor-pointer"
|
||||
>
|
||||
<span>{type.replace(/_/g, " ")}</span>
|
||||
<span className="text-xs text-muted-foreground">{typeCounts[type]}</span>
|
||||
<span className="text-xs text-muted-foreground">{typeCounts?.[type]}</span>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -435,7 +447,7 @@ export function DocumentsDataTable({
|
|||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{selectedCount} selected {loading && "· Loading..."}
|
||||
{selectedCount} selected {actualLoading && "· Loading..."}
|
||||
</span>
|
||||
<div className="hidden sm:block h-4 w-px bg-border mx-2" />
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
|
|
@ -453,7 +465,7 @@ export function DocumentsDataTable({
|
|||
size="sm"
|
||||
onClick={handleSelectPage}
|
||||
className="text-xs sm:text-sm"
|
||||
disabled={loading}
|
||||
disabled={actualLoading}
|
||||
>
|
||||
Select Page
|
||||
</Button>
|
||||
|
|
@ -490,7 +502,7 @@ export function DocumentsDataTable({
|
|||
{/* Table Container */}
|
||||
<div className="border rounded-lg flex-1 min-h-0 overflow-hidden bg-background">
|
||||
<div className="overflow-auto h-full">
|
||||
{loading ? (
|
||||
{actualLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full mx-auto" />
|
||||
|
|
@ -561,31 +573,31 @@ export function DocumentsDataTable({
|
|||
{/* Footer Pagination */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs sm:text-sm text-muted-foreground border-t pt-3 md:pt-4 flex-shrink-0">
|
||||
<div className="text-center sm:text-left">
|
||||
Showing {pageIndex * pageSize + 1} to {Math.min((pageIndex + 1) * pageSize, total)} of{" "}
|
||||
{total} documents
|
||||
Showing {pageIndex * pageSize + 1} to {Math.min((pageIndex + 1) * pageSize, actualTotal)} of{" "}
|
||||
{actualTotal} documents
|
||||
</div>
|
||||
<div className="flex items-center justify-center sm:justify-end space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPageIndex((p) => Math.max(0, p - 1))}
|
||||
disabled={pageIndex === 0 || loading}
|
||||
className="text-xs sm:text-sm"
|
||||
size="sm"
|
||||
onClick={() => setPageIndex((p) => Math.max(0, p - 1))}
|
||||
disabled={pageIndex === 0 || actualLoading}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<div className="flex items-center space-x-1 text-xs sm:text-sm">
|
||||
<span>Page</span>
|
||||
<strong>{pageIndex + 1}</strong>
|
||||
<span>of</span>
|
||||
<strong>{Math.ceil(total / pageSize)}</strong>
|
||||
</div>
|
||||
<strong>{pageIndex + 1}</strong>
|
||||
<span>of</span>
|
||||
<strong>{Math.ceil(actualTotal / pageSize)}</strong>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPageIndex((p) => p + 1)}
|
||||
disabled={pageIndex >= Math.ceil(total / pageSize) - 1 || loading}
|
||||
className="text-xs sm:text-sm"
|
||||
size="sm"
|
||||
onClick={() => setPageIndex((p) => p + 1)}
|
||||
disabled={pageIndex >= Math.ceil(actualTotal / pageSize) - 1 || actualLoading}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronDown, ChevronUp, ExternalLink, Loader2 } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
|
|
@ -15,7 +16,8 @@ import {
|
|||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import { useDocumentByChunk } from "@/hooks/use-document-by-chunk";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SourceDetailSheetProps {
|
||||
|
|
@ -46,11 +48,22 @@ export function SourceDetailSheet({
|
|||
url,
|
||||
children,
|
||||
}: SourceDetailSheetProps) {
|
||||
const { document, loading, error, fetchDocumentByChunk, clearDocument } = useDocumentByChunk();
|
||||
const chunksContainerRef = useRef<HTMLDivElement>(null);
|
||||
const highlightedChunkRef = useRef<HTMLDivElement>(null);
|
||||
const [summaryOpen, setSummaryOpen] = useState(false);
|
||||
|
||||
// Add useQuery to fetch document by chunk
|
||||
const {
|
||||
data: document,
|
||||
isLoading: isDocumentByChunkFetching,
|
||||
error: documentByChunkFetchingError,
|
||||
} = useQuery({
|
||||
queryKey: cacheKeys.documents.byChunk(chunkId.toString()),
|
||||
queryFn: () => documentsApiService.getDocumentByChunk({ chunk_id: chunkId }),
|
||||
enabled: !!chunkId && open,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
|
||||
// Check if this is a source type that should render directly from node
|
||||
const isDirectRenderSource =
|
||||
sourceType === "TAVILY_API" ||
|
||||
|
|
@ -58,17 +71,9 @@ export function SourceDetailSheet({
|
|||
sourceType === "SEARXNG_API" ||
|
||||
sourceType === "BAIDU_SEARCH_API";
|
||||
|
||||
useEffect(() => {
|
||||
if (open && chunkId && !isDirectRenderSource) {
|
||||
fetchDocumentByChunk(chunkId);
|
||||
} else if (!open && !isDirectRenderSource) {
|
||||
clearDocument();
|
||||
}
|
||||
}, [open, chunkId, isDirectRenderSource, fetchDocumentByChunk, clearDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
// Scroll to highlighted chunk when document loads
|
||||
if (document && highlightedChunkRef.current && chunksContainerRef.current) {
|
||||
if (document) {
|
||||
setTimeout(() => {
|
||||
highlightedChunkRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
|
|
@ -76,7 +81,7 @@ export function SourceDetailSheet({
|
|||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [document]);
|
||||
}, [document, open]);
|
||||
|
||||
const handleUrlClick = (e: React.MouseEvent, clickUrl: string) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -100,15 +105,17 @@ export function SourceDetailSheet({
|
|||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{!isDirectRenderSource && loading && (
|
||||
{!isDirectRenderSource && isDocumentByChunkFetching && (
|
||||
<div className="flex items-center justify-center h-64 px-6">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isDirectRenderSource && error && (
|
||||
{!isDirectRenderSource && documentByChunkFetchingError && (
|
||||
<div className="flex items-center justify-center h-64 px-6">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
<p className="text-sm text-destructive">
|
||||
{documentByChunkFetchingError.message || "Failed to load document"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useAtom } from "jotai";
|
||||
import { CheckCircle2, FileType, Info, Loader2, Tag, Upload, X } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
@ -7,6 +8,7 @@ import { useTranslations } from "next-intl";
|
|||
import { useCallback, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { toast } from "sonner";
|
||||
import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -14,7 +16,6 @@ import { Button } from "@/components/ui/button";
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { getAuthHeaders } from "@/lib/auth-utils";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
|
||||
interface DocumentUploadTabProps {
|
||||
|
|
@ -25,9 +26,12 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
|
|||
const t = useTranslations("upload_documents");
|
||||
const router = useRouter();
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
// Use the uploadDocumentMutationAtom
|
||||
const [uploadDocumentMutation] = useAtom(uploadDocumentMutationAtom);
|
||||
const { mutate: uploadDocuments, isPending: isUploading } = uploadDocumentMutation;
|
||||
|
||||
const audioFileTypes = {
|
||||
"audio/mpeg": [".mp3", ".mpeg", ".mpga"],
|
||||
"audio/mp4": [".mp4", ".m4a"],
|
||||
|
|
@ -148,53 +152,40 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
|
|||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
});
|
||||
formData.append("search_space_id", searchSpaceId);
|
||||
// Create a progress interval to simulate progress
|
||||
const progressInterval = setInterval(() => {
|
||||
setUploadProgress((prev) => {
|
||||
if (prev >= 90) return prev;
|
||||
return prev + Math.random() * 10;
|
||||
});
|
||||
}, 200);
|
||||
|
||||
try {
|
||||
const progressInterval = setInterval(() => {
|
||||
setUploadProgress((prev) => {
|
||||
if (prev >= 90) return prev;
|
||||
return prev + Math.random() * 10;
|
||||
});
|
||||
}, 200);
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/fileupload`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setUploadProgress(100);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Upload failed");
|
||||
// Use the mutation to upload documents
|
||||
uploadDocuments(
|
||||
{
|
||||
files,
|
||||
search_space_id: Number(searchSpaceId),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
clearInterval(progressInterval);
|
||||
setUploadProgress(100);
|
||||
toast(t("upload_initiated"), {
|
||||
description: t("upload_initiated_desc"),
|
||||
});
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
clearInterval(progressInterval);
|
||||
setUploadProgress(0);
|
||||
toast(t("upload_error"), {
|
||||
description: `${t("upload_error_desc")}: ${error.message || "Upload failed"}`,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
await response.json();
|
||||
|
||||
toast(t("upload_initiated"), {
|
||||
description: t("upload_initiated_desc"),
|
||||
});
|
||||
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
} catch (error: any) {
|
||||
setIsUploading(false);
|
||||
setUploadProgress(0);
|
||||
toast(t("upload_error"), {
|
||||
description: `${t("upload_error_desc")}: ${error.message}`,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getTotalFileSize = () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import { motion } from "motion/react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { toast } from "sonner";
|
||||
import { createDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -19,7 +21,6 @@ import {
|
|||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
|
||||
const youtubeRegex =
|
||||
/^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/;
|
||||
|
|
@ -33,9 +34,12 @@ export function YouTubeTab({ searchSpaceId }: YouTubeTabProps) {
|
|||
const router = useRouter();
|
||||
const [videoTags, setVideoTags] = useState<TagType[]>([]);
|
||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Use the createDocumentMutationAtom
|
||||
const [createDocumentMutation] = useAtom(createDocumentMutationAtom);
|
||||
const { mutate: createYouTubeDocument, isPending: isSubmitting } = createDocumentMutation;
|
||||
|
||||
const isValidYoutubeUrl = (url: string): boolean => {
|
||||
return youtubeRegex.test(url);
|
||||
};
|
||||
|
|
@ -58,47 +62,35 @@ export function YouTubeTab({ searchSpaceId }: YouTubeTabProps) {
|
|||
}
|
||||
|
||||
setError(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
toast(t("processing_toast"), {
|
||||
description: t("processing_toast_desc"),
|
||||
});
|
||||
toast(t("processing_toast"), {
|
||||
description: t("processing_toast_desc"),
|
||||
});
|
||||
|
||||
const videoUrls = videoTags.map((tag) => tag.text);
|
||||
const videoUrls = videoTags.map((tag) => tag.text);
|
||||
|
||||
const response = await authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
document_type: "YOUTUBE_VIDEO",
|
||||
content: videoUrls,
|
||||
search_space_id: parseInt(searchSpaceId),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to process YouTube videos");
|
||||
// Use the mutation to create YouTube documents
|
||||
createYouTubeDocument(
|
||||
{
|
||||
document_type: "YOUTUBE_VIDEO",
|
||||
content: videoUrls,
|
||||
search_space_id: parseInt(searchSpaceId),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast(t("success_toast"), {
|
||||
description: t("success_toast_desc"),
|
||||
});
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setError(error.message || t("error_generic"));
|
||||
toast(t("error_toast"), {
|
||||
description: `${t("error_toast_desc")}: ${error.message || "Failed to process YouTube videos"}`,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
await response.json();
|
||||
|
||||
toast(t("success_toast"), {
|
||||
description: t("success_toast_desc"),
|
||||
});
|
||||
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
} catch (error: any) {
|
||||
setError(error.message || t("error_generic"));
|
||||
toast(t("error_toast"), {
|
||||
description: `${t("error_toast_desc")}: ${error.message}`,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddTag = (text: string) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue