UI: Fix document loading speed, skeleton

This commit is contained in:
Adityavardhan Agrawal 2025-04-27 19:49:42 -07:00
parent e8b7f6789b
commit b9bc1d3566
6 changed files with 179 additions and 85 deletions

View File

@ -20,6 +20,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Skeleton } from "@/components/ui/skeleton";
// Dynamically import ForceGraphComponent to avoid SSR issues // Dynamically import ForceGraphComponent to avoid SSR issues
const ForceGraphComponent = dynamic(() => import('@/components/ForceGraphComponent'), { ssr: false }); const ForceGraphComponent = dynamic(() => import('@/components/ForceGraphComponent'), { ssr: false });
@ -516,7 +517,15 @@ const GraphSection: React.FC<GraphSectionProps> = ({ apiBaseUrl, onSelectGraph,
{loading ? ( {loading ? (
<div className="flex justify-center items-center p-8"> <div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary"></div> {/* Skeleton Loader for Graph List */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 py-2 w-full">
{[...Array(12)].map((_, i) => (
<div key={i} className="flex flex-col items-center p-2 border border-transparent rounded-md">
<Skeleton className="h-12 w-12 mb-2 rounded-md" />
<Skeleton className="h-4 w-20 rounded-md" />
</div>
))}
</div>
</div> </div>
) : graphs.length === 0 ? ( ) : graphs.length === 0 ? (
<div className="text-center p-8 border-2 border-dashed rounded-lg mt-4"> <div className="text-center p-8 border-2 border-dashed rounded-lg mt-4">

View File

@ -8,6 +8,7 @@ import DocumentDetail from './DocumentDetail';
import FolderList from './FolderList'; import FolderList from './FolderList';
import { UploadDialog, useUploadDialog } from './UploadDialog'; import { UploadDialog, useUploadDialog } from './UploadDialog';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Skeleton } from "@/components/ui/skeleton";
import { Document, Folder } from '@/components/types'; import { Document, Folder } from '@/components/types';
@ -237,7 +238,7 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({
setLoading(false); setLoading(false);
} }
// Dependencies: URL, auth, selected folder, and the folder list itself // Dependencies: URL, auth, selected folder, and the folder list itself
}, [effectiveApiUrl, authToken, selectedFolder, folders, documents.length]); }, [effectiveApiUrl, authToken, selectedFolder, folders]);
// Fetch all folders // Fetch all folders
const fetchFolders = useCallback(async () => { const fetchFolders = useCallback(async () => {
@ -282,47 +283,103 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({
// Fetch documents when folders are loaded or selectedFolder changes // Fetch documents when folders are loaded or selectedFolder changes
useEffect(() => { useEffect(() => {
if (!foldersLoading && folders.length > 0) { const effectSource = 'folders loaded or selectedFolder changed';
console.log(`Effect triggered: ${effectSource}, foldersLoading: ${foldersLoading}, folders count: ${folders.length}, selectedFolder: ${selectedFolder}`);
// Guard against running when folders are still loading
if (foldersLoading) {
console.log(`Effect (${effectSource}): Folders still loading, skipping.`);
return;
}
// Handle the case where there are no folders at all
if (folders.length === 0 && selectedFolder === null) {
console.log(`Effect (${effectSource}): No folders found, clearing documents and stopping loading.`);
setDocuments([]);
setLoading(false); // Ensure loading is off
isInitialMount.current = false;
return;
}
// Proceed if folders are loaded
if (folders.length >= 0) { // Check >= 0 to handle empty folders array correctly
// Avoid fetching documents on initial mount if selectedFolder is null // Avoid fetching documents on initial mount if selectedFolder is null
// unless initialFolder was specified // unless initialFolder was specified
if (isInitialMount.current && selectedFolder === null && !initialFolder) { if (isInitialMount.current && selectedFolder === null && !initialFolder) {
console.log('Initial mount with no folder selected, skipping document fetch'); console.log(`Effect (${effectSource}): Initial mount with no folder selected, skipping document fetch`);
isInitialMount.current = false; isInitialMount.current = false;
// Ensure loading is false if we skip fetching
setLoading(false);
return; return;
} }
console.log('Folders loaded or selectedFolder changed, fetching documents...', selectedFolder);
fetchDocuments('folders loaded or selectedFolder changed');
isInitialMount.current = false; // Mark initial mount as complete
} else if (!foldersLoading && folders.length === 0 && selectedFolder === null) {
// Handle case where there are no folders at all
console.log('No folders found, clearing documents and stopping loading.');
setDocuments([]);
setLoading(false);
isInitialMount.current = false;
}
}, [foldersLoading, folders, selectedFolder, fetchDocuments, initialFolder]);
// Poll for document status if any document is processing // If we reach here, we intend to fetch documents
console.log(`Effect (${effectSource}): Preparing to fetch documents for folder: ${selectedFolder}`);
// Wrap the async operation
const fetchWrapper = async () => {
// Explicitly set loading true *before* the async call within this effect's scope
// Note: fetchDocuments might also set this, but we ensure it's set here.
setLoading(true);
try {
await fetchDocuments(effectSource);
// If fetchDocuments completes successfully, it will set loading = false in its finally block.
// No need to set it here again in the try block.
console.log(`Effect (${effectSource}): fetchDocuments call completed.`);
} catch (error) {
// Catch potential errors *from* the await fetchDocuments call itself, though
// fetchDocuments has internal handling. This is an extra safeguard.
console.error(`Effect (${effectSource}): Error occurred during fetchDocuments call:`, error);
showAlert(`Error updating documents: ${error instanceof Error ? error.message : 'Unknown error'}`, { type: 'error' });
// Ensure loading is turned off even if fetchDocuments had an issue before its finally.
setLoading(false);
} finally {
// **User Request:** Explicitly set loading to false within the effect's finally block.
// This acts as a safeguard, ensuring loading is false after the attempt,
// regardless of fetchDocuments' internal state management.
console.log(`Effect (${effectSource}): Finally block reached, ensuring loading is false.`);
setLoading(false);
isInitialMount.current = false; // Mark initial mount as complete here
}
};
fetchWrapper();
} else {
console.log(`Effect (${effectSource}): Condition not met (folders length < 0 ?), should not happen.`);
setLoading(false); // Fallback
}
}, [foldersLoading, folders, selectedFolder, fetchDocuments, initialFolder]); // Keep fetchDocuments dependency
// Poll for document status if any document is processing *and* user is not viewing details
useEffect(() => { useEffect(() => {
const hasProcessing = documents.some( const hasProcessing = documents.some(
(doc) => doc.system_metadata?.status === 'processing' (doc) => doc.system_metadata?.status === 'processing'
); );
if (hasProcessing) { // Only poll if there are processing documents AND no document detail view is open
console.log('Polling for document status...'); if (hasProcessing && selectedDocument === null) {
console.log('Polling for document status (list view active)...');
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
console.log('Polling interval: calling refreshDocuments'); console.log('Polling interval: calling refreshDocuments');
refreshDocuments(); // Fetch documents again to check status refreshDocuments(); // Fetch documents again to check status
}, 5000); // Poll every 5 seconds }, 5000); // Poll every 5 seconds
// Cleanup function to clear the interval when the component unmounts
// or when there are no more processing documents
return () => { return () => {
console.log('Clearing polling interval'); console.log('Clearing polling interval (list view or processing done)');
clearInterval(intervalId); clearInterval(intervalId);
}; };
} else {
// Log why polling is not active
if (hasProcessing && selectedDocument !== null) {
console.log('Polling paused: Document detail view is open.');
} else if (!hasProcessing) {
console.log('Polling stopped: No documents are processing.');
}
} }
}, [documents, refreshDocuments]); // Add selectedDocument to dependencies
}, [documents, refreshDocuments, selectedDocument]);
// Collapse sidebar when a folder is selected // Collapse sidebar when a folder is selected
useEffect(() => { useEffect(() => {
@ -1017,68 +1074,89 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({
/> />
)} )}
{selectedFolder && documents.length === 0 && !loading ? ( {/* Folder Grid View (selectedFolder is null) */}
<div className="text-center py-8 border border-dashed rounded-lg flex-1 flex items-center justify-center"> {selectedFolder === null ? (
<div>
<Upload className="mx-auto h-12 w-12 mb-2 text-muted-foreground" />
<p className="text-muted-foreground">Drag and drop files here to upload to this folder.</p>
<p className="text-xs text-muted-foreground mt-2">Or use the upload button in the top right.</p>
</div>
</div>
) : selectedFolder && loading ? (
<div className="text-center py-8 flex-1 flex items-center justify-center">
<div className="flex flex-col items-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mb-4"></div>
<p className="text-muted-foreground">Loading documents...</p>
</div>
</div>
) : selectedFolder === null ? (
<div className="flex flex-col gap-4 flex-1"> <div className="flex flex-col gap-4 flex-1">
<FolderList {/* Skeleton for Folder List loading state */}
folders={folders} {foldersLoading ? (
selectedFolder={selectedFolder} <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 p-4">
setSelectedFolder={setSelectedFolder} {[...Array(8)].map((_, i) => (
apiBaseUrl={effectiveApiUrl} <div key={i} className="border rounded-lg p-4 flex flex-col items-center justify-center h-32">
authToken={authToken} <Skeleton className="h-8 w-8 mb-2 rounded-md" />
refreshFolders={fetchFolders} <Skeleton className="h-4 w-20" />
loading={foldersLoading} </div>
refreshAction={handleRefresh} ))}
selectedDocuments={selectedDocuments} </div>
handleDeleteMultipleDocuments={handleDeleteMultipleDocuments} ) : (
uploadDialogComponent={ <FolderList
<UploadDialog folders={folders}
showUploadDialog={showUploadDialog} selectedFolder={selectedFolder}
setShowUploadDialog={setShowUploadDialog} setSelectedFolder={setSelectedFolder}
loading={loading} apiBaseUrl={effectiveApiUrl}
onFileUpload={handleFileUpload} authToken={authToken}
onBatchFileUpload={handleBatchFileUpload} refreshFolders={fetchFolders}
onTextUpload={handleTextUpload} loading={foldersLoading}
/> refreshAction={handleRefresh}
} selectedDocuments={selectedDocuments}
/> handleDeleteMultipleDocuments={handleDeleteMultipleDocuments}
uploadDialogComponent={
<UploadDialog
showUploadDialog={showUploadDialog}
setShowUploadDialog={setShowUploadDialog}
loading={loading}
onFileUpload={handleFileUpload}
onBatchFileUpload={handleBatchFileUpload}
onTextUpload={handleTextUpload}
/>
}
/>
)}
</div> </div>
) : ( ) : (
<div className="flex flex-col md:flex-row gap-4 flex-1"> <div className="flex flex-col md:flex-row gap-4 flex-1">
{/* Left Panel: Document List or Skeleton or Empty State */}
<div className={cn( <div className={cn(
"w-full transition-all duration-300", "w-full transition-all duration-300 flex flex-col",
selectedDocument ? "md:w-2/3" : "md:w-full" selectedDocument ? "md:w-2/3" : "md:w-full"
)}> )}>
<DocumentList {loading ? (
documents={documents} // Skeleton Loader for Document List (within the correct layout)
selectedDocument={selectedDocument} <div className="space-y-3 p-4 flex-1">
selectedDocuments={selectedDocuments} <Skeleton className="h-10 w-full" />
handleDocumentClick={handleDocumentClick} <Skeleton className="h-8 w-3/4" />
handleCheckboxChange={handleCheckboxChange} <Skeleton className="h-8 w-full" />
getSelectAllState={getSelectAllState} <Skeleton className="h-8 w-5/6" />
setSelectedDocuments={setSelectedDocuments} <Skeleton className="h-8 w-full" />
setDocuments={setDocuments} </div>
loading={loading} ) : documents.length === 0 ? (
apiBaseUrl={effectiveApiUrl} // Empty State (moved here, ensure it fills space)
authToken={authToken} <div className="text-center py-8 border border-dashed rounded-lg flex-1 flex items-center justify-center">
selectedFolder={selectedFolder} <div>
/> <Upload className="mx-auto h-12 w-12 mb-2 text-muted-foreground" />
<p className="text-muted-foreground">Drag and drop files here to upload to this folder.</p>
<p className="text-xs text-muted-foreground mt-2">Or use the upload button in the top right.</p>
</div>
</div>
) : (
// Actual Document List
<DocumentList
documents={documents}
selectedDocument={selectedDocument}
selectedDocuments={selectedDocuments}
handleDocumentClick={handleDocumentClick}
handleCheckboxChange={handleCheckboxChange}
getSelectAllState={getSelectAllState}
setSelectedDocuments={setSelectedDocuments}
setDocuments={setDocuments}
loading={loading}
apiBaseUrl={effectiveApiUrl}
authToken={authToken}
selectedFolder={selectedFolder}
/>
)}
</div> </div>
{/* Right Panel: Document Detail (conditionally rendered) */}
{selectedDocument && ( {selectedDocument && (
<div className="w-full md:w-1/3 animate-in slide-in-from-right duration-300"> <div className="w-full md:w-1/3 animate-in slide-in-from-right duration-300">
<DocumentDetail <DocumentDetail

View File

@ -1,3 +1,6 @@
"use client"
import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Skeleton({ function Skeleton({

View File

@ -1,6 +1,6 @@
{ {
"name": "@morphik/ui", "name": "@morphik/ui",
"version": "0.1.7", "version": "0.2.0",
"private": true, "private": true,
"description": "Modern UI component for Morphik - A powerful document processing and querying system", "description": "Modern UI component for Morphik - A powerful document processing and querying system",
"author": "Morphik Team", "author": "Morphik Team",
@ -36,7 +36,6 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/react": "^1.2.9",
"@radix-ui/primitive": "^1.1.2", "@radix-ui/primitive": "^1.1.2",
"@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-checkbox": "^1.1.5", "@radix-ui/react-checkbox": "^1.1.5",
@ -51,10 +50,8 @@
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.2.4",
"accessor-fn": "^1.5.3", "accessor-fn": "^1.5.3",
"accordion": "^3.0.2", "accordion": "^3.0.2",
"ai": "^4.3.10",
"alert": "^6.0.2", "alert": "^6.0.2",
"caniuse-lite": "^1.0.30001714", "caniuse-lite": "^1.0.30001714",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -79,7 +76,7 @@
}, },
"devDependencies": { "devDependencies": {
"@shadcn/ui": "^0.0.4", "@shadcn/ui": "^0.0.4",
"@types/node": "^20.17.31", "@types/node": "^20",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"eslint": "^8", "eslint": "^8",
@ -90,17 +87,16 @@
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"peerDependencies": { "peerDependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"next": "^14", "next": "^14",
"next-themes": "^0.4.6",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"next-themes": "^0.4.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, }
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

3
ee/ui-component/react-shim.js vendored Normal file
View File

@ -0,0 +1,3 @@
import * as React from "react";
export { React };

View File

@ -16,4 +16,9 @@ export default defineConfig({
'next/link', 'next/link',
'@radix-ui/*', '@radix-ui/*',
], ],
esbuildOptions(options) {
options.jsx = 'automatic';
options.jsxImportSource = 'react';
return options;
},
}); });