mirror of
https://github.com/james-m-jordan/morphik-core.git
synced 2025-05-09 19:32:38 +00:00
Add folders to the UI component (#89)
This commit is contained in:
parent
aa2315847a
commit
2b1c253bc1
@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
|||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
import { QueryOptions } from '@/components/types';
|
import { QueryOptions, Folder } from '@/components/types';
|
||||||
|
|
||||||
interface ChatOptionsDialogProps {
|
interface ChatOptionsDialogProps {
|
||||||
showChatAdvanced: boolean;
|
showChatAdvanced: boolean;
|
||||||
@ -18,6 +18,7 @@ interface ChatOptionsDialogProps {
|
|||||||
queryOptions: QueryOptions;
|
queryOptions: QueryOptions;
|
||||||
updateQueryOption: <K extends keyof QueryOptions>(key: K, value: QueryOptions[K]) => void;
|
updateQueryOption: <K extends keyof QueryOptions>(key: K, value: QueryOptions[K]) => void;
|
||||||
availableGraphs: string[];
|
availableGraphs: string[];
|
||||||
|
folders: Folder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatOptionsDialog: React.FC<ChatOptionsDialogProps> = ({
|
const ChatOptionsDialog: React.FC<ChatOptionsDialogProps> = ({
|
||||||
@ -25,7 +26,8 @@ const ChatOptionsDialog: React.FC<ChatOptionsDialogProps> = ({
|
|||||||
setShowChatAdvanced,
|
setShowChatAdvanced,
|
||||||
queryOptions,
|
queryOptions,
|
||||||
updateQueryOption,
|
updateQueryOption,
|
||||||
availableGraphs
|
availableGraphs,
|
||||||
|
folders
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={showChatAdvanced} onOpenChange={setShowChatAdvanced}>
|
<Dialog open={showChatAdvanced} onOpenChange={setShowChatAdvanced}>
|
||||||
@ -152,6 +154,29 @@ const ChatOptionsDialog: React.FC<ChatOptionsDialogProps> = ({
|
|||||||
Select a knowledge graph to enhance your query with structured relationships
|
Select a knowledge graph to enhance your query with structured relationships
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="folderName" className="block mb-2">Scope to Folder</Label>
|
||||||
|
<Select
|
||||||
|
value={queryOptions.folder_name || "__none__"}
|
||||||
|
onValueChange={(value) => updateQueryOption('folder_name', value === "__none__" ? undefined : value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full" id="folderName">
|
||||||
|
<SelectValue placeholder="Select a folder" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">All Folders</SelectItem>
|
||||||
|
{folders.map(folder => (
|
||||||
|
<SelectItem key={folder.name} value={folder.name}>
|
||||||
|
{folder.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Limit chat results to documents within a specific folder
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
@ -10,7 +10,7 @@ import { showAlert } from '@/components/ui/alert-system';
|
|||||||
import ChatOptionsDialog from './ChatOptionsDialog';
|
import ChatOptionsDialog from './ChatOptionsDialog';
|
||||||
import ChatMessageComponent from './ChatMessage';
|
import ChatMessageComponent from './ChatMessage';
|
||||||
|
|
||||||
import { ChatMessage, QueryOptions } from '@/components/types';
|
import { ChatMessage, QueryOptions, Folder } from '@/components/types';
|
||||||
|
|
||||||
interface ChatSectionProps {
|
interface ChatSectionProps {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
@ -23,6 +23,7 @@ const ChatSection: React.FC<ChatSectionProps> = ({ apiBaseUrl, authToken }) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showChatAdvanced, setShowChatAdvanced] = useState(false);
|
const [showChatAdvanced, setShowChatAdvanced] = useState(false);
|
||||||
const [availableGraphs, setAvailableGraphs] = useState<string[]>([]);
|
const [availableGraphs, setAvailableGraphs] = useState<string[]>([]);
|
||||||
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
const [queryOptions, setQueryOptions] = useState<QueryOptions>({
|
const [queryOptions, setQueryOptions] = useState<QueryOptions>({
|
||||||
filters: '{}',
|
filters: '{}',
|
||||||
k: 4,
|
k: 4,
|
||||||
@ -59,13 +60,35 @@ const ChatSection: React.FC<ChatSectionProps> = ({ apiBaseUrl, authToken }) => {
|
|||||||
}
|
}
|
||||||
}, [apiBaseUrl, authToken]);
|
}, [apiBaseUrl, authToken]);
|
||||||
|
|
||||||
// Fetch graphs when auth token or API URL changes
|
// Fetch graphs and folders when auth token or API URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
console.log('ChatSection: Fetching graphs with new auth token');
|
console.log('ChatSection: Fetching data with new auth token');
|
||||||
// Clear current messages when auth changes
|
// Clear current messages when auth changes
|
||||||
setChatMessages([]);
|
setChatMessages([]);
|
||||||
fetchGraphs();
|
fetchGraphs();
|
||||||
|
|
||||||
|
// Fetch available folders for dropdown
|
||||||
|
const fetchFolders = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/folders`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': authToken ? `Bearer ${authToken}` : ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch folders: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foldersData = await response.json();
|
||||||
|
setFolders(foldersData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching folders:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchFolders();
|
||||||
}
|
}
|
||||||
}, [authToken, apiBaseUrl, fetchGraphs]);
|
}, [authToken, apiBaseUrl, fetchGraphs]);
|
||||||
|
|
||||||
@ -86,7 +109,7 @@ const ChatSection: React.FC<ChatSectionProps> = ({ apiBaseUrl, authToken }) => {
|
|||||||
const userMessage: ChatMessage = { role: 'user', content: chatQuery };
|
const userMessage: ChatMessage = { role: 'user', content: chatQuery };
|
||||||
setChatMessages(prev => [...prev, userMessage]);
|
setChatMessages(prev => [...prev, userMessage]);
|
||||||
|
|
||||||
// Prepare options with graph_name if it exists
|
// Prepare options with graph_name and folder_name if they exist
|
||||||
const options = {
|
const options = {
|
||||||
filters: JSON.parse(queryOptions.filters || '{}'),
|
filters: JSON.parse(queryOptions.filters || '{}'),
|
||||||
k: queryOptions.k,
|
k: queryOptions.k,
|
||||||
@ -95,7 +118,8 @@ const ChatSection: React.FC<ChatSectionProps> = ({ apiBaseUrl, authToken }) => {
|
|||||||
use_colpali: queryOptions.use_colpali,
|
use_colpali: queryOptions.use_colpali,
|
||||||
max_tokens: queryOptions.max_tokens,
|
max_tokens: queryOptions.max_tokens,
|
||||||
temperature: queryOptions.temperature,
|
temperature: queryOptions.temperature,
|
||||||
graph_name: queryOptions.graph_name
|
graph_name: queryOptions.graph_name,
|
||||||
|
folder_name: queryOptions.folder_name
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`${apiBaseUrl}/query`, {
|
const response = await fetch(`${apiBaseUrl}/query`, {
|
||||||
@ -193,6 +217,7 @@ const ChatSection: React.FC<ChatSectionProps> = ({ apiBaseUrl, authToken }) => {
|
|||||||
queryOptions={queryOptions}
|
queryOptions={queryOptions}
|
||||||
updateQueryOption={updateQueryOption}
|
updateQueryOption={updateQueryOption}
|
||||||
availableGraphs={availableGraphs}
|
availableGraphs={availableGraphs}
|
||||||
|
folders={folders}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,26 +1,39 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
import { Info } from 'lucide-react';
|
import { Info, Folder as FolderIcon } from 'lucide-react';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
||||||
import { Document } from '@/components/types';
|
import { Document, Folder } from '@/components/types';
|
||||||
|
|
||||||
interface DocumentDetailProps {
|
interface DocumentDetailProps {
|
||||||
selectedDocument: Document | null;
|
selectedDocument: Document | null;
|
||||||
handleDeleteDocument: (documentId: string) => Promise<void>;
|
handleDeleteDocument: (documentId: string) => Promise<void>;
|
||||||
|
folders: Folder[];
|
||||||
|
apiBaseUrl: string;
|
||||||
|
authToken: string | null;
|
||||||
|
refreshDocuments: () => void;
|
||||||
|
refreshFolders: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
||||||
selectedDocument,
|
selectedDocument,
|
||||||
handleDeleteDocument,
|
handleDeleteDocument,
|
||||||
|
folders,
|
||||||
|
apiBaseUrl,
|
||||||
|
authToken,
|
||||||
|
refreshDocuments,
|
||||||
|
refreshFolders,
|
||||||
loading
|
loading
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isMovingToFolder, setIsMovingToFolder] = useState(false);
|
||||||
|
|
||||||
if (!selectedDocument) {
|
if (!selectedDocument) {
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-200px)] flex items-center justify-center p-8 border border-dashed rounded-lg">
|
<div className="h-[calc(100vh-200px)] flex items-center justify-center p-8 border border-dashed rounded-lg">
|
||||||
@ -32,6 +45,62 @@ const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentFolder = selectedDocument.system_metadata?.folder_name as string | undefined;
|
||||||
|
|
||||||
|
const handleMoveToFolder = async (folderName: string | null) => {
|
||||||
|
if (isMovingToFolder || !selectedDocument) return;
|
||||||
|
|
||||||
|
const documentId = selectedDocument.external_id;
|
||||||
|
setIsMovingToFolder(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, get the folder ID from the name if a name is provided
|
||||||
|
if (folderName) {
|
||||||
|
// Find the target folder by name
|
||||||
|
const targetFolder = folders.find(folder => folder.name === folderName);
|
||||||
|
if (targetFolder && targetFolder.id) {
|
||||||
|
console.log(`Found folder with ID: ${targetFolder.id} for name: ${folderName}`);
|
||||||
|
|
||||||
|
// Add to folder using folder ID
|
||||||
|
await fetch(`${apiBaseUrl}/folders/${targetFolder.id}/documents/${documentId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(`Could not find folder with name: ${folderName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a current folder and we're either moving to a new folder or removing from folder
|
||||||
|
if (currentFolder) {
|
||||||
|
// Find the current folder ID
|
||||||
|
const currentFolderObj = folders.find(folder => folder.name === currentFolder);
|
||||||
|
if (currentFolderObj && currentFolderObj.id) {
|
||||||
|
// Remove from current folder using folder ID
|
||||||
|
await fetch(`${apiBaseUrl}/folders/${currentFolderObj.id}/documents/${documentId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh folders first to get updated document_ids
|
||||||
|
await refreshFolders();
|
||||||
|
// Then refresh documents with the updated folder information
|
||||||
|
await refreshDocuments();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating folder:', error);
|
||||||
|
} finally {
|
||||||
|
setIsMovingToFolder(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border rounded-lg">
|
<div className="border rounded-lg">
|
||||||
<div className="bg-muted px-4 py-3 border-b sticky top-0">
|
<div className="bg-muted px-4 py-3 border-b sticky top-0">
|
||||||
@ -47,7 +116,31 @@ const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium mb-1">Content Type</h3>
|
<h3 className="font-medium mb-1">Content Type</h3>
|
||||||
<Badge>{selectedDocument.content_type}</Badge>
|
<Badge variant="secondary">{selectedDocument.content_type}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-1">Folder</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FolderIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<Select
|
||||||
|
value={currentFolder || "_none"}
|
||||||
|
onValueChange={(value) => handleMoveToFolder(value === "_none" ? null : value)}
|
||||||
|
disabled={isMovingToFolder}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Not in a folder" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="_none">Not in a folder</SelectItem>
|
||||||
|
{folders.map((folder) => (
|
||||||
|
<SelectItem key={folder.name} value={folder.name}>
|
||||||
|
{folder.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -32,6 +32,9 @@ const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
return <div className="text-center py-8 flex-1">Loading documents...</div>;
|
return <div className="text-center py-8 flex-1">Loading documents...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status badge helper component (used in the document list items)
|
||||||
|
// Status rendering is handled inline in the component instead
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border rounded-md">
|
<div className="border rounded-md">
|
||||||
<div className="bg-muted border-b p-3 font-medium sticky top-0">
|
<div className="bg-muted border-b p-3 font-medium sticky top-0">
|
||||||
@ -51,8 +54,23 @@ const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4">Filename</div>
|
<div className="col-span-4">Filename</div>
|
||||||
<div className="col-span-3">Type</div>
|
<div className="col-span-2">Type</div>
|
||||||
<div className="col-span-4">ID</div>
|
<div className="col-span-2">
|
||||||
|
<div className="group relative inline-flex items-center">
|
||||||
|
Status
|
||||||
|
<span className="ml-1 text-muted-foreground cursor-help">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||||
|
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div className="absolute left-0 -top-24 hidden group-hover:block bg-gray-800 text-white text-xs p-2 rounded w-60 z-50 shadow-lg">
|
||||||
|
Documents with "Processing" status are queryable, but visual features like direct visual context will only be available after processing completes.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">ID</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -61,7 +79,9 @@ const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
<div
|
<div
|
||||||
key={doc.external_id}
|
key={doc.external_id}
|
||||||
onClick={() => handleDocumentClick(doc)}
|
onClick={() => handleDocumentClick(doc)}
|
||||||
className="grid grid-cols-12 p-3 cursor-pointer hover:bg-muted/50 border-b"
|
className={`grid grid-cols-12 p-3 cursor-pointer hover:bg-muted/50 border-b ${
|
||||||
|
doc.external_id === selectedDocument?.external_id ? 'bg-muted' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="col-span-1 flex items-center justify-center">
|
<div className="col-span-1 flex items-center justify-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -73,21 +93,44 @@ const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 flex items-center">
|
<div className="col-span-4 flex items-center">
|
||||||
{doc.filename || 'N/A'}
|
<span className="truncate">{doc.filename || 'N/A'}</span>
|
||||||
{doc.external_id === selectedDocument?.external_id && (
|
|
||||||
<Badge variant="outline" className="ml-2">Selected</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-3">
|
<div className="col-span-2">
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
{doc.content_type.split('/')[0]}
|
{doc.content_type.split('/')[0]}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 font-mono text-xs">
|
<div className="col-span-2">
|
||||||
|
{doc.system_metadata?.status === "completed" ? (
|
||||||
|
<Badge variant="outline" className="bg-green-50 text-green-800 border-green-200">
|
||||||
|
Completed
|
||||||
|
</Badge>
|
||||||
|
) : doc.system_metadata?.status === "failed" ? (
|
||||||
|
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">
|
||||||
|
Failed
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<div className="group relative flex items-center">
|
||||||
|
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 px-2 py-1">
|
||||||
|
Processing
|
||||||
|
</Badge>
|
||||||
|
<div className="absolute left-0 -bottom-10 hidden group-hover:block bg-gray-800 text-white text-xs p-2 rounded whitespace-nowrap z-10">
|
||||||
|
Document is being processed. Partial search available.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 font-mono text-xs">
|
||||||
{doc.external_id.substring(0, 8)}...
|
{doc.external_id.substring(0, 8)}...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{documents.length === 0 && (
|
||||||
|
<div className="p-8 text-center text-muted-foreground">
|
||||||
|
No documents found in this view. Try uploading a document or selecting a different folder.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Upload } from 'lucide-react';
|
import { Upload } from 'lucide-react';
|
||||||
import { showAlert, removeAlert } from '@/components/ui/alert-system';
|
import { showAlert, removeAlert } from '@/components/ui/alert-system';
|
||||||
import DocumentList from './DocumentList';
|
import DocumentList from './DocumentList';
|
||||||
import DocumentDetail from './DocumentDetail';
|
import DocumentDetail from './DocumentDetail';
|
||||||
|
import FolderList from './FolderList';
|
||||||
import { UploadDialog, useUploadDialog } from './UploadDialog';
|
import { UploadDialog, useUploadDialog } from './UploadDialog';
|
||||||
|
|
||||||
import { Document } from '@/components/types';
|
import { Document, Folder } from '@/components/types';
|
||||||
|
|
||||||
interface DocumentsSectionProps {
|
interface DocumentsSectionProps {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
authToken: string | null;
|
authToken: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug render counter
|
||||||
|
let renderCount = 0;
|
||||||
|
|
||||||
const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authToken }) => {
|
const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authToken }) => {
|
||||||
|
// Increment render counter for debugging
|
||||||
|
renderCount++;
|
||||||
|
console.log(`DocumentsSection rendered: #${renderCount}`);
|
||||||
// Ensure apiBaseUrl is correctly formatted, especially for localhost
|
// Ensure apiBaseUrl is correctly formatted, especially for localhost
|
||||||
const effectiveApiUrl = React.useMemo(() => {
|
const effectiveApiUrl = React.useMemo(() => {
|
||||||
console.log('DocumentsSection: Input apiBaseUrl:', apiBaseUrl);
|
console.log('DocumentsSection: Input apiBaseUrl:', apiBaseUrl);
|
||||||
@ -27,11 +34,17 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
}
|
}
|
||||||
return apiBaseUrl;
|
return apiBaseUrl;
|
||||||
}, [apiBaseUrl]);
|
}, [apiBaseUrl]);
|
||||||
// State for documents
|
|
||||||
|
// State for documents and folders
|
||||||
const [documents, setDocuments] = useState<Document[]>([]);
|
const [documents, setDocuments] = useState<Document[]>([]);
|
||||||
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
|
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
|
||||||
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
|
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
|
||||||
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
|
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [foldersLoading, setFoldersLoading] = useState(false);
|
||||||
|
// Use ref to track if this is the initial mount
|
||||||
|
const isInitialMount = useRef(true);
|
||||||
|
|
||||||
// Upload dialog state from custom hook
|
// Upload dialog state from custom hook
|
||||||
const uploadDialogState = useUploadDialog();
|
const uploadDialogState = useUploadDialog();
|
||||||
@ -45,32 +58,77 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
resetUploadDialog
|
resetUploadDialog
|
||||||
} = uploadDialogState;
|
} = uploadDialogState;
|
||||||
|
|
||||||
// Headers for API requests - ensure this updates when props change
|
// No need for a separate header function, use authToken directly
|
||||||
const headers = React.useMemo(() => {
|
|
||||||
return {
|
|
||||||
'Authorization': authToken ? `Bearer ${authToken}` : ''
|
|
||||||
};
|
|
||||||
}, [authToken]);
|
|
||||||
|
|
||||||
// Fetch all documents
|
// Fetch all documents, optionally filtered by folder
|
||||||
const fetchDocuments = useCallback(async () => {
|
const fetchDocuments = useCallback(async (source: string = 'unknown') => {
|
||||||
|
console.log(`fetchDocuments called from: ${source}`)
|
||||||
try {
|
try {
|
||||||
// Only set loading state for initial load, not for refreshes
|
// Only set loading state for initial load, not for refreshes
|
||||||
if (documents.length === 0) {
|
if (documents.length === 0) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('DocumentsSection: Sending request to:', `${effectiveApiUrl}/documents`);
|
// Don't fetch if no folder is selected (showing folder grid view)
|
||||||
console.log('DocumentsSection: Headers:', JSON.stringify(headers));
|
if (selectedFolder === null) {
|
||||||
|
setDocuments([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use non-blocking fetch
|
// Prepare for document fetching
|
||||||
fetch(`${effectiveApiUrl}/documents`, {
|
let apiUrl = `${effectiveApiUrl}/documents`;
|
||||||
method: 'POST',
|
// CRITICAL FIX: The /documents endpoint uses POST method
|
||||||
|
let method = 'POST';
|
||||||
|
let requestBody = {};
|
||||||
|
|
||||||
|
// If we're looking at a specific folder (not "all" documents)
|
||||||
|
if (selectedFolder && selectedFolder !== "all") {
|
||||||
|
console.log(`Fetching documents for folder: ${selectedFolder}`);
|
||||||
|
|
||||||
|
// Find the target folder to get its document IDs
|
||||||
|
const targetFolder = folders.find(folder => folder.name === selectedFolder);
|
||||||
|
|
||||||
|
if (targetFolder) {
|
||||||
|
// Ensure document_ids is always an array
|
||||||
|
const documentIds = Array.isArray(targetFolder.document_ids) ? targetFolder.document_ids : [];
|
||||||
|
|
||||||
|
if (documentIds.length > 0) {
|
||||||
|
// If we found the folder and it contains documents,
|
||||||
|
// Get document details for each document ID in the folder
|
||||||
|
console.log(`Found folder ${targetFolder.name} with ${documentIds.length} documents`);
|
||||||
|
|
||||||
|
// Use batch/documents endpoint which accepts document_ids for efficient fetching
|
||||||
|
apiUrl = `${effectiveApiUrl}/batch/documents`;
|
||||||
|
method = 'POST';
|
||||||
|
requestBody = {
|
||||||
|
document_ids: [...documentIds]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(`Folder ${targetFolder.name} has no documents`);
|
||||||
|
// For empty folder, we'll send an empty request body to the documents endpoint
|
||||||
|
requestBody = {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Folder ${selectedFolder} has no documents or couldn't be found`);
|
||||||
|
// For unknown folder, we'll send an empty request body to the documents endpoint
|
||||||
|
requestBody = {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For "all" documents request
|
||||||
|
requestBody = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`DocumentsSection: Sending ${method} request to: ${apiUrl}`);
|
||||||
|
|
||||||
|
// Use non-blocking fetch with appropriate method
|
||||||
|
fetch(apiUrl, {
|
||||||
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
},
|
},
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify(requestBody)
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -78,12 +136,37 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then((data: Document[]) => {
|
||||||
setDocuments(data);
|
// Ensure all documents have a valid status in system_metadata
|
||||||
|
const processedData = data.map((doc: Document) => {
|
||||||
|
// If system_metadata doesn't exist, create it
|
||||||
|
if (!doc.system_metadata) {
|
||||||
|
doc.system_metadata = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status is missing and we have a newly uploaded document, it should be "processing"
|
||||||
|
if (!doc.system_metadata.status && doc.system_metadata.folder_name) {
|
||||||
|
doc.system_metadata.status = "processing";
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Fetched ${processedData.length} documents from ${apiUrl}`);
|
||||||
|
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
setDocuments(processedData);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
const processingCount = processedData.filter(doc => doc.system_metadata?.status === "processing").length;
|
||||||
|
if (processingCount > 0) {
|
||||||
|
console.log(`Found ${processingCount} documents still processing`);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||||
|
console.error(`Document fetch error: ${errorMsg}`);
|
||||||
showAlert(errorMsg, {
|
showAlert(errorMsg, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
@ -100,29 +183,275 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [effectiveApiUrl, authToken, headers, documents.length]);
|
}, [effectiveApiUrl, authToken, documents.length, selectedFolder, folders]);
|
||||||
|
|
||||||
// Fetch documents when auth token or API URL changes (but not when fetchDocuments changes)
|
// Fetch all folders
|
||||||
|
const fetchFolders = useCallback(async (source: string = 'unknown') => {
|
||||||
|
console.log(`fetchFolders called from: ${source}`)
|
||||||
|
try {
|
||||||
|
setFoldersLoading(true);
|
||||||
|
|
||||||
|
// Use non-blocking fetch with GET method
|
||||||
|
const url = `${effectiveApiUrl}/folders`;
|
||||||
|
console.log(`Fetching folders from: ${url}`);
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch folders: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log(`Fetched ${data.length} folders`);
|
||||||
|
setFolders(data);
|
||||||
|
setFoldersLoading(false);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||||
|
console.error(`Error fetching folders: ${errorMsg}`);
|
||||||
|
showAlert(`Error fetching folders: ${errorMsg}`, {
|
||||||
|
type: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
setFoldersLoading(false);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||||
|
console.error(`Error in fetchFolders: ${errorMsg}`);
|
||||||
|
setFoldersLoading(false);
|
||||||
|
}
|
||||||
|
}, [effectiveApiUrl, authToken]);
|
||||||
|
|
||||||
|
// No automatic polling - we'll only refresh on upload and manual refresh button clicks
|
||||||
|
|
||||||
|
// Create memoized fetch functions that don't cause infinite loops
|
||||||
|
// We need to make sure they don't trigger re-renders that cause further fetches
|
||||||
|
const stableFetchFolders = useCallback((source: string = 'stable-call') => {
|
||||||
|
console.log(`stableFetchFolders called from: ${source}`);
|
||||||
|
return fetchFolders(source);
|
||||||
|
// Keep dependencies minimal to prevent recreation on every render
|
||||||
|
}, [effectiveApiUrl, authToken]);
|
||||||
|
|
||||||
|
const stableFetchDocuments = useCallback((source: string = 'stable-call') => {
|
||||||
|
console.log(`stableFetchDocuments called from: ${source}`);
|
||||||
|
return fetchDocuments(source);
|
||||||
|
// Keep dependencies minimal to prevent recreation on every render
|
||||||
|
}, [effectiveApiUrl, authToken, selectedFolder]);
|
||||||
|
|
||||||
|
// Fetch data when auth token or API URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Only run this effect if we have auth or are on localhost
|
||||||
if (authToken || effectiveApiUrl.includes('localhost')) {
|
if (authToken || effectiveApiUrl.includes('localhost')) {
|
||||||
console.log('DocumentsSection: Fetching documents on auth/API change');
|
console.log('DocumentsSection: Fetching initial data');
|
||||||
|
|
||||||
// Clear current documents and reset state
|
// Clear current data and reset state
|
||||||
setDocuments([]);
|
setDocuments([]);
|
||||||
|
setFolders([]);
|
||||||
setSelectedDocument(null);
|
setSelectedDocument(null);
|
||||||
fetchDocuments();
|
setSelectedDocuments([]);
|
||||||
|
|
||||||
|
// Use a flag to track component mounting state
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
// Create an abort controller for request cancellation
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// Add a slight delay to prevent multiple rapid calls
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (isMounted) {
|
||||||
|
// Fetch folders first
|
||||||
|
stableFetchFolders('initial-load')
|
||||||
|
.then(() => {
|
||||||
|
// Only fetch documents if we're still mounted
|
||||||
|
if (isMounted && selectedFolder !== null) {
|
||||||
|
return stableFetchDocuments('initial-load');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error during initial data fetch:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Cleanup when component unmounts or the effect runs again
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
isMounted = false;
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [authToken, effectiveApiUrl]);
|
}, [authToken, effectiveApiUrl, stableFetchFolders, stableFetchDocuments, selectedFolder]);
|
||||||
|
|
||||||
|
// Helper function to refresh documents based on current view
|
||||||
|
const refreshDocuments = async (folders: Folder[]) => {
|
||||||
|
try {
|
||||||
|
if (selectedFolder && selectedFolder !== "all") {
|
||||||
|
// Find the folder by name
|
||||||
|
const targetFolder = folders.find(folder => folder.name === selectedFolder);
|
||||||
|
|
||||||
|
if (targetFolder) {
|
||||||
|
console.log(`Refresh: Found folder ${targetFolder.name} in fresh data`);
|
||||||
|
|
||||||
|
// Get the document IDs from the folder
|
||||||
|
const documentIds = Array.isArray(targetFolder.document_ids) ? targetFolder.document_ids : [];
|
||||||
|
console.log(`Refresh: Folder has ${documentIds.length} documents`);
|
||||||
|
|
||||||
|
if (documentIds.length > 0) {
|
||||||
|
// Fetch document details for the IDs
|
||||||
|
const docResponse = await fetch(`${effectiveApiUrl}/batch/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
document_ids: [...documentIds]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!docResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch documents: ${docResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const freshDocs = await docResponse.json();
|
||||||
|
console.log(`Refresh: Fetched ${freshDocs.length} document details`);
|
||||||
|
|
||||||
|
// Update documents state
|
||||||
|
setDocuments(freshDocs);
|
||||||
|
} else {
|
||||||
|
// Empty folder
|
||||||
|
setDocuments([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Refresh: Selected folder ${selectedFolder} not found in fresh data`);
|
||||||
|
setDocuments([]);
|
||||||
|
}
|
||||||
|
} else if (selectedFolder === "all") {
|
||||||
|
// For "all" documents view, fetch all documents
|
||||||
|
const allDocsResponse = await fetch(`${effectiveApiUrl}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allDocsResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch all documents: ${allDocsResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allDocs = await allDocsResponse.json();
|
||||||
|
console.log(`Refresh: Fetched ${allDocs.length} documents for "all" view`);
|
||||||
|
setDocuments(allDocs);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing documents:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch documents when selected folder changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip initial render to prevent double fetching with the auth useEffect
|
||||||
|
if (isInitialMount.current) {
|
||||||
|
isInitialMount.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Folder selection changed to: ${selectedFolder}`);
|
||||||
|
|
||||||
|
// Clear selected document when changing folders
|
||||||
|
setSelectedDocument(null);
|
||||||
|
setSelectedDocuments([]);
|
||||||
|
|
||||||
|
// CRITICAL: Clear document list immediately to prevent showing old documents
|
||||||
|
// This prevents showing documents from previous folders while loading
|
||||||
|
setDocuments([]);
|
||||||
|
|
||||||
|
// Create a flag to handle component unmounting
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
// Create an abort controller for fetch operations
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// Only fetch if we have a valid auth token or running locally
|
||||||
|
if ((authToken || effectiveApiUrl.includes('localhost')) && isMounted) {
|
||||||
|
// Add a small delay to prevent rapid consecutive calls
|
||||||
|
const timeoutId = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
// Set loading state to show we're fetching new data
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Start with a fresh folder fetch
|
||||||
|
const folderResponse = await fetch(`${effectiveApiUrl}/folders`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
},
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folderResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch folders: ${folderResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get fresh folder data
|
||||||
|
const freshFolders = await folderResponse.json();
|
||||||
|
console.log(`Folder change: Fetched ${freshFolders.length} folders with fresh data`);
|
||||||
|
|
||||||
|
// Update folder state
|
||||||
|
if (isMounted) {
|
||||||
|
setFolders(freshFolders);
|
||||||
|
|
||||||
|
// Then fetch documents with fresh folder data
|
||||||
|
await refreshDocuments(freshFolders);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.name !== 'AbortError') {
|
||||||
|
console.error("Error during folder change fetch:", err);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Clean up timeout if unmounted
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
isMounted = false;
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function to prevent updates after unmount
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
// Only depend on these specific props to prevent infinite loops
|
||||||
|
}, [selectedFolder, authToken, effectiveApiUrl]);
|
||||||
|
|
||||||
// Fetch a specific document by ID
|
// Fetch a specific document by ID
|
||||||
const fetchDocument = async (documentId: string) => {
|
const fetchDocument = async (documentId: string) => {
|
||||||
try {
|
try {
|
||||||
console.log('DocumentsSection: Fetching document detail from:', `${effectiveApiUrl}/documents/${documentId}`);
|
const url = `${effectiveApiUrl}/documents/${documentId}`;
|
||||||
|
console.log('DocumentsSection: Fetching document detail from:', url);
|
||||||
|
|
||||||
// Use non-blocking fetch to avoid locking the UI
|
// Use non-blocking fetch to avoid locking the UI
|
||||||
fetch(`${effectiveApiUrl}/documents/${documentId}`, {
|
fetch(url, {
|
||||||
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -131,21 +460,33 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
console.log(`Fetched document details for ID: ${documentId}`);
|
||||||
|
|
||||||
|
// Ensure document has a valid status in system_metadata
|
||||||
|
if (!data.system_metadata) {
|
||||||
|
data.system_metadata = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status is missing and we have a newly uploaded document, it should be "processing"
|
||||||
|
if (!data.system_metadata.status && data.system_metadata.folder_name) {
|
||||||
|
data.system_metadata.status = "processing";
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedDocument(data);
|
setSelectedDocument(data);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||||
showAlert(errorMsg, {
|
console.error(`Error fetching document details: ${errorMsg}`);
|
||||||
|
showAlert(`Error fetching document: ${errorMsg}`, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Error',
|
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||||
showAlert(errorMsg, {
|
console.error(`Error in fetchDocument: ${errorMsg}`);
|
||||||
|
showAlert(`Error: ${errorMsg}`, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Error',
|
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -184,7 +525,8 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
setSelectedDocument(null);
|
setSelectedDocument(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh documents list
|
// Refresh folders first, then documents
|
||||||
|
await fetchFolders();
|
||||||
await fetchDocuments();
|
await fetchDocuments();
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
@ -241,7 +583,8 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
// Clear selection
|
// Clear selection
|
||||||
setSelectedDocuments([]);
|
setSelectedDocuments([]);
|
||||||
|
|
||||||
// Refresh documents list
|
// Refresh folders first, then documents
|
||||||
|
await fetchFolders();
|
||||||
await fetchDocuments();
|
await fetchDocuments();
|
||||||
|
|
||||||
// Remove progress alert
|
// Remove progress alert
|
||||||
@ -328,6 +671,26 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
formData.append('metadata', metadataRef);
|
formData.append('metadata', metadataRef);
|
||||||
formData.append('rules', rulesRef);
|
formData.append('rules', rulesRef);
|
||||||
|
|
||||||
|
// If we're in a specific folder (not "all" documents), add the folder_name to form data
|
||||||
|
if (selectedFolder && selectedFolder !== "all") {
|
||||||
|
try {
|
||||||
|
// Parse metadata to validate it's proper JSON, but don't modify it
|
||||||
|
JSON.parse(metadataRef || '{}');
|
||||||
|
|
||||||
|
// The API expects folder_name as a direct Form parameter
|
||||||
|
// This will be used by document_service._ensure_folder_exists()
|
||||||
|
formData.set('metadata', metadataRef);
|
||||||
|
formData.append('folder_name', selectedFolder);
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
console.log(`Adding file to folder: ${selectedFolder} as form field`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing metadata:', e);
|
||||||
|
formData.set('metadata', metadataRef);
|
||||||
|
formData.append('folder_name', selectedFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const url = `${effectiveApiUrl}/ingest/file${useColpaliRef ? '?use_colpali=true' : ''}`;
|
const url = `${effectiveApiUrl}/ingest/file${useColpaliRef ? '?use_colpali=true' : ''}`;
|
||||||
|
|
||||||
// Non-blocking fetch
|
// Non-blocking fetch
|
||||||
@ -344,8 +707,48 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then((newDocument) => {
|
||||||
fetchDocuments(); // Refresh document list (non-blocking)
|
// Log processing status of uploaded document
|
||||||
|
if (newDocument && newDocument.system_metadata && newDocument.system_metadata.status === "processing") {
|
||||||
|
console.log(`Document ${newDocument.external_id} is in processing status`);
|
||||||
|
// No longer need to track processing documents for polling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force a fresh refresh after upload
|
||||||
|
// This is a special function to ensure we get truly fresh data
|
||||||
|
const refreshAfterUpload = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Performing fresh refresh after upload");
|
||||||
|
// Clear folder data to force a clean refresh
|
||||||
|
setFolders([]);
|
||||||
|
|
||||||
|
// Get fresh folder data from the server
|
||||||
|
const folderResponse = await fetch(`${effectiveApiUrl}/folders`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folderResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch folders: ${folderResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const freshFolders = await folderResponse.json();
|
||||||
|
console.log(`Upload: Fetched ${freshFolders.length} folders with fresh data`);
|
||||||
|
|
||||||
|
// Update folders state with fresh data
|
||||||
|
setFolders(freshFolders);
|
||||||
|
|
||||||
|
// Now fetch documents based on the current view
|
||||||
|
await refreshDocuments(freshFolders);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error refreshing after upload:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the refresh
|
||||||
|
refreshAfterUpload();
|
||||||
|
|
||||||
// Show success message and remove upload progress
|
// Show success message and remove upload progress
|
||||||
showAlert(`File uploaded successfully!`, {
|
showAlert(`File uploaded successfully!`, {
|
||||||
@ -423,7 +826,18 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
formData.append('files', file);
|
formData.append('files', file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add metadata to all cases
|
||||||
formData.append('metadata', metadataRef);
|
formData.append('metadata', metadataRef);
|
||||||
|
|
||||||
|
// If we're in a specific folder (not "all" documents), add the folder_name as a separate field
|
||||||
|
if (selectedFolder && selectedFolder !== "all") {
|
||||||
|
// The API expects folder_name directly, not ID
|
||||||
|
formData.append('folder_name', selectedFolder);
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
console.log(`Adding batch files to folder: ${selectedFolder} as form field`);
|
||||||
|
}
|
||||||
|
|
||||||
formData.append('rules', rulesRef);
|
formData.append('rules', rulesRef);
|
||||||
formData.append('parallel', 'true');
|
formData.append('parallel', 'true');
|
||||||
if (useColpaliRef !== undefined) {
|
if (useColpaliRef !== undefined) {
|
||||||
@ -445,7 +859,47 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
fetchDocuments(); // Refresh document list (non-blocking)
|
// Log processing status of uploaded documents
|
||||||
|
if (result && result.document_ids && result.document_ids.length > 0) {
|
||||||
|
console.log(`${result.document_ids.length} documents are in processing status`);
|
||||||
|
// No need for polling, just wait for manual refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force a fresh refresh after upload
|
||||||
|
// This is a special function to ensure we get truly fresh data
|
||||||
|
const refreshAfterUpload = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Performing fresh refresh after upload");
|
||||||
|
// Clear folder data to force a clean refresh
|
||||||
|
setFolders([]);
|
||||||
|
|
||||||
|
// Get fresh folder data from the server
|
||||||
|
const folderResponse = await fetch(`${effectiveApiUrl}/folders`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folderResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch folders: ${folderResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const freshFolders = await folderResponse.json();
|
||||||
|
console.log(`Upload: Fetched ${freshFolders.length} folders with fresh data`);
|
||||||
|
|
||||||
|
// Update folders state with fresh data
|
||||||
|
setFolders(freshFolders);
|
||||||
|
|
||||||
|
// Now fetch documents based on the current view
|
||||||
|
await refreshDocuments(freshFolders);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error refreshing after upload:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the refresh
|
||||||
|
refreshAfterUpload();
|
||||||
|
|
||||||
// If there are errors, show them in the error alert
|
// If there are errors, show them in the error alert
|
||||||
if (result.errors && result.errors.length > 0) {
|
if (result.errors && result.errors.length > 0) {
|
||||||
@ -518,7 +972,23 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
|
|
||||||
// Save content before resetting
|
// Save content before resetting
|
||||||
const textContentRef = text;
|
const textContentRef = text;
|
||||||
const metadataRef = meta;
|
let metadataObj = {};
|
||||||
|
let folderToUse = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
metadataObj = JSON.parse(meta || '{}');
|
||||||
|
|
||||||
|
// If we're in a specific folder (not "all" documents), set folder variable
|
||||||
|
if (selectedFolder && selectedFolder !== "all") {
|
||||||
|
// The API expects the folder name directly
|
||||||
|
folderToUse = selectedFolder;
|
||||||
|
// Log for debugging
|
||||||
|
console.log(`Will add text document to folder: ${selectedFolder}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing metadata JSON:', e);
|
||||||
|
}
|
||||||
|
|
||||||
const rulesRef = rulesText;
|
const rulesRef = rulesText;
|
||||||
const useColpaliRef = useColpaliFlag;
|
const useColpaliRef = useColpaliFlag;
|
||||||
|
|
||||||
@ -535,9 +1005,10 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: textContentRef,
|
content: textContentRef,
|
||||||
metadata: JSON.parse(metadataRef || '{}'),
|
metadata: metadataObj,
|
||||||
rules: JSON.parse(rulesRef || '[]'),
|
rules: JSON.parse(rulesRef || '[]'),
|
||||||
use_colpali: useColpaliRef
|
use_colpali: useColpaliRef,
|
||||||
|
folder_name: folderToUse
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@ -546,8 +1017,48 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then((newDocument) => {
|
||||||
fetchDocuments(); // Refresh document list (non-blocking)
|
// Log processing status of uploaded document
|
||||||
|
if (newDocument && newDocument.system_metadata && newDocument.system_metadata.status === "processing") {
|
||||||
|
console.log(`Document ${newDocument.external_id} is in processing status`);
|
||||||
|
// No longer need to track processing documents for polling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force a fresh refresh after upload
|
||||||
|
// This is a special function to ensure we get truly fresh data
|
||||||
|
const refreshAfterUpload = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Performing fresh refresh after upload");
|
||||||
|
// Clear folder data to force a clean refresh
|
||||||
|
setFolders([]);
|
||||||
|
|
||||||
|
// Get fresh folder data from the server
|
||||||
|
const folderResponse = await fetch(`${effectiveApiUrl}/folders`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folderResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch folders: ${folderResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const freshFolders = await folderResponse.json();
|
||||||
|
console.log(`Upload: Fetched ${freshFolders.length} folders with fresh data`);
|
||||||
|
|
||||||
|
// Update folders state with fresh data
|
||||||
|
setFolders(freshFolders);
|
||||||
|
|
||||||
|
// Now fetch documents based on the current view
|
||||||
|
await refreshDocuments(freshFolders);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error refreshing after upload:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the refresh
|
||||||
|
refreshAfterUpload();
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
showAlert(`Text document uploaded successfully!`, {
|
showAlert(`Text document uploaded successfully!`, {
|
||||||
@ -588,12 +1099,19 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Title based on selected folder
|
||||||
|
const sectionTitle = selectedFolder === null
|
||||||
|
? "Folders"
|
||||||
|
: selectedFolder === "all"
|
||||||
|
? "All Documents"
|
||||||
|
: `Folder: ${selectedFolder}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col h-full">
|
<div className="flex-1 flex flex-col h-full">
|
||||||
<div className="flex justify-between items-center py-3 mb-4">
|
<div className="flex justify-between items-center py-3 mb-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold leading-tight">Your Documents</h2>
|
<h2 className="text-2xl font-bold leading-tight">{sectionTitle}</h2>
|
||||||
<p className="text-muted-foreground">Manage your uploaded documents and view their metadata.</p>
|
<p className="text-muted-foreground">Manage your uploaded documents and view their metadata.</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedDocuments.length > 0 && (
|
{selectedDocuments.length > 0 && (
|
||||||
@ -607,17 +1125,89 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<UploadDialog
|
<div className="flex items-center gap-2">
|
||||||
showUploadDialog={showUploadDialog}
|
<Button
|
||||||
setShowUploadDialog={setShowUploadDialog}
|
variant="outline"
|
||||||
loading={loading}
|
onClick={() => {
|
||||||
onFileUpload={handleFileUpload}
|
console.log("Manual refresh triggered");
|
||||||
onBatchFileUpload={handleBatchFileUpload}
|
// Show a loading indicator
|
||||||
onTextUpload={handleTextUpload}
|
showAlert("Refreshing documents and folders...", {
|
||||||
/>
|
type: 'info',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
|
||||||
|
// First clear folder data to force a clean refresh
|
||||||
|
setLoading(true);
|
||||||
|
setFolders([]);
|
||||||
|
|
||||||
|
// Create a new function to perform a truly fresh fetch
|
||||||
|
const performFreshFetch = async () => {
|
||||||
|
try {
|
||||||
|
// First get fresh folder data from the server
|
||||||
|
const folderResponse = await fetch(`${effectiveApiUrl}/folders`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folderResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch folders: ${folderResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the fresh folder data
|
||||||
|
const freshFolders = await folderResponse.json();
|
||||||
|
console.log(`Refresh: Fetched ${freshFolders.length} folders with fresh data`);
|
||||||
|
|
||||||
|
// Update folders state with fresh data
|
||||||
|
setFolders(freshFolders);
|
||||||
|
|
||||||
|
// Use our helper function to refresh documents with fresh folder data
|
||||||
|
await refreshDocuments(freshFolders);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showAlert("Refresh completed successfully", {
|
||||||
|
type: 'success',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during refresh:", error);
|
||||||
|
showAlert(`Error refreshing: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
|
type: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the fresh fetch
|
||||||
|
performFreshFetch();
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center"
|
||||||
|
title="Refresh documents"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-1">
|
||||||
|
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
|
||||||
|
<path d="M21 3v5h-5"></path>
|
||||||
|
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
|
||||||
|
<path d="M8 16H3v5"></path>
|
||||||
|
</svg>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<UploadDialog
|
||||||
|
showUploadDialog={showUploadDialog}
|
||||||
|
setShowUploadDialog={setShowUploadDialog}
|
||||||
|
loading={loading}
|
||||||
|
onFileUpload={handleFileUpload}
|
||||||
|
onBatchFileUpload={handleBatchFileUpload}
|
||||||
|
onTextUpload={handleTextUpload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{documents.length === 0 && !loading ? (
|
{documents.length === 0 && !loading && folders.length === 0 && !foldersLoading ? (
|
||||||
<div className="text-center py-8 border border-dashed rounded-lg flex-1 flex items-center justify-center">
|
<div className="text-center py-8 border border-dashed rounded-lg flex-1 flex items-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<Upload className="mx-auto h-12 w-12 mb-2 text-muted-foreground" />
|
<Upload className="mx-auto h-12 w-12 mb-2 text-muted-foreground" />
|
||||||
@ -625,27 +1215,46 @@ const DocumentsSection: React.FC<DocumentsSectionProps> = ({ apiBaseUrl, authTok
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col md:flex-row gap-4 flex-1">
|
<div className="flex flex-col gap-4 flex-1">
|
||||||
<div className="w-full md:w-2/3">
|
<FolderList
|
||||||
<DocumentList
|
folders={folders}
|
||||||
documents={documents}
|
selectedFolder={selectedFolder}
|
||||||
selectedDocument={selectedDocument}
|
setSelectedFolder={setSelectedFolder}
|
||||||
selectedDocuments={selectedDocuments}
|
apiBaseUrl={effectiveApiUrl}
|
||||||
handleDocumentClick={handleDocumentClick}
|
authToken={authToken}
|
||||||
handleCheckboxChange={handleCheckboxChange}
|
refreshFolders={fetchFolders}
|
||||||
getSelectAllState={getSelectAllState}
|
loading={foldersLoading}
|
||||||
setSelectedDocuments={setSelectedDocuments}
|
/>
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full md:w-1/3">
|
{selectedFolder !== null && (
|
||||||
<DocumentDetail
|
<div className="flex flex-col md:flex-row gap-4 flex-1">
|
||||||
selectedDocument={selectedDocument}
|
<div className="w-full md:w-2/3">
|
||||||
handleDeleteDocument={handleDeleteDocument}
|
<DocumentList
|
||||||
loading={loading}
|
documents={documents}
|
||||||
/>
|
selectedDocument={selectedDocument}
|
||||||
</div>
|
selectedDocuments={selectedDocuments}
|
||||||
|
handleDocumentClick={handleDocumentClick}
|
||||||
|
handleCheckboxChange={handleCheckboxChange}
|
||||||
|
getSelectAllState={getSelectAllState}
|
||||||
|
setSelectedDocuments={setSelectedDocuments}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full md:w-1/3">
|
||||||
|
<DocumentDetail
|
||||||
|
selectedDocument={selectedDocument}
|
||||||
|
handleDeleteDocument={handleDeleteDocument}
|
||||||
|
folders={folders}
|
||||||
|
apiBaseUrl={effectiveApiUrl}
|
||||||
|
authToken={authToken}
|
||||||
|
refreshDocuments={fetchDocuments}
|
||||||
|
refreshFolders={fetchFolders}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
214
ui-component/components/documents/FolderList.tsx
Normal file
214
ui-component/components/documents/FolderList.tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { PlusCircle, Folder as FolderIcon, File, ArrowLeft } from 'lucide-react';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Folder } from '@/components/types';
|
||||||
|
|
||||||
|
interface FolderListProps {
|
||||||
|
folders: Folder[];
|
||||||
|
selectedFolder: string | null;
|
||||||
|
setSelectedFolder: (folderName: string | null) => void;
|
||||||
|
apiBaseUrl: string;
|
||||||
|
authToken: string | null;
|
||||||
|
refreshFolders: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FolderList: React.FC<FolderListProps> = ({
|
||||||
|
folders,
|
||||||
|
selectedFolder,
|
||||||
|
setSelectedFolder,
|
||||||
|
apiBaseUrl,
|
||||||
|
authToken,
|
||||||
|
refreshFolders,
|
||||||
|
loading
|
||||||
|
}) => {
|
||||||
|
const [showNewFolderDialog, setShowNewFolderDialog] = React.useState(false);
|
||||||
|
const [newFolderName, setNewFolderName] = React.useState('');
|
||||||
|
const [newFolderDescription, setNewFolderDescription] = React.useState('');
|
||||||
|
const [isCreatingFolder, setIsCreatingFolder] = React.useState(false);
|
||||||
|
|
||||||
|
const handleCreateFolder = async () => {
|
||||||
|
if (!newFolderName.trim()) return;
|
||||||
|
|
||||||
|
setIsCreatingFolder(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Creating folder: ${newFolderName}`);
|
||||||
|
|
||||||
|
const response = await fetch(`${apiBaseUrl}/folders`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newFolderName.trim(),
|
||||||
|
description: newFolderDescription.trim() || undefined
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to create folder: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the created folder data
|
||||||
|
const folderData = await response.json();
|
||||||
|
console.log(`Created folder with ID: ${folderData.id} and name: ${folderData.name}`);
|
||||||
|
|
||||||
|
// Close dialog and reset form
|
||||||
|
setShowNewFolderDialog(false);
|
||||||
|
setNewFolderName('');
|
||||||
|
setNewFolderDescription('');
|
||||||
|
|
||||||
|
// Refresh folder list - use a fresh fetch
|
||||||
|
await refreshFolders();
|
||||||
|
|
||||||
|
// Auto-select this newly created folder so user can immediately add files to it
|
||||||
|
// This ensures we start with a clean empty folder view
|
||||||
|
setSelectedFolder(folderData.name);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating folder:', error);
|
||||||
|
} finally {
|
||||||
|
setIsCreatingFolder(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we're viewing a specific folder or all documents, show back button and folder title
|
||||||
|
if (selectedFolder !== null) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="p-1 h-8 w-8"
|
||||||
|
onClick={() => setSelectedFolder(null)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
</Button>
|
||||||
|
<h2 className="font-medium text-lg flex items-center">
|
||||||
|
{selectedFolder === "all" ? (
|
||||||
|
<>
|
||||||
|
<File className="h-5 w-5 mr-2" />
|
||||||
|
All Documents
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FolderIcon className="h-5 w-5 mr-2" />
|
||||||
|
{selectedFolder}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="font-medium text-lg">Folders</h2>
|
||||||
|
<Dialog open={showNewFolderDialog} onOpenChange={setShowNewFolderDialog}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<PlusCircle className="h-4 w-4 mr-2" /> New Folder
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Folder</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new folder to organize your documents.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="folderName">Folder Name</Label>
|
||||||
|
<Input
|
||||||
|
id="folderName"
|
||||||
|
value={newFolderName}
|
||||||
|
onChange={(e) => setNewFolderName(e.target.value)}
|
||||||
|
placeholder="My Folder"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="folderDescription">Description (Optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
id="folderDescription"
|
||||||
|
value={newFolderDescription}
|
||||||
|
onChange={(e) => setNewFolderDescription(e.target.value)}
|
||||||
|
placeholder="Enter a description for this folder"
|
||||||
|
className="mt-1"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowNewFolderDialog(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateFolder} disabled={isCreatingFolder || !newFolderName.trim()}>
|
||||||
|
{isCreatingFolder ? 'Creating...' : 'Create Folder'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer hover:border-primary transition-colors",
|
||||||
|
"flex flex-col items-center justify-center h-24"
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedFolder("all")}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4 flex flex-col items-center justify-center">
|
||||||
|
<File className="h-10 w-10 mb-1" />
|
||||||
|
<span className="text-sm font-medium text-center">All Documents</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{folders.map((folder) => (
|
||||||
|
<Card
|
||||||
|
key={folder.name}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer hover:border-primary transition-colors",
|
||||||
|
"flex flex-col items-center justify-center h-24"
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedFolder(folder.name)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4 flex flex-col items-center justify-center">
|
||||||
|
<FolderIcon className="h-10 w-10 mb-1" />
|
||||||
|
<span className="text-sm font-medium truncate text-center w-full">{folder.name}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{folders.length === 0 && !loading && (
|
||||||
|
<div className="text-center p-8 text-sm text-muted-foreground">
|
||||||
|
No folders yet. Create one to organize your documents.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading && folders.length === 0 && (
|
||||||
|
<div className="text-center p-8 text-sm text-muted-foreground">
|
||||||
|
Loading folders...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FolderList;
|
@ -8,21 +8,24 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
import { SearchOptions } from '@/components/types';
|
import { SearchOptions, Folder } from '@/components/types';
|
||||||
|
|
||||||
interface SearchOptionsDialogProps {
|
interface SearchOptionsDialogProps {
|
||||||
showSearchAdvanced: boolean;
|
showSearchAdvanced: boolean;
|
||||||
setShowSearchAdvanced: (show: boolean) => void;
|
setShowSearchAdvanced: (show: boolean) => void;
|
||||||
searchOptions: SearchOptions;
|
searchOptions: SearchOptions;
|
||||||
updateSearchOption: <K extends keyof SearchOptions>(key: K, value: SearchOptions[K]) => void;
|
updateSearchOption: <K extends keyof SearchOptions>(key: K, value: SearchOptions[K]) => void;
|
||||||
|
folders: Folder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchOptionsDialog: React.FC<SearchOptionsDialogProps> = ({
|
const SearchOptionsDialog: React.FC<SearchOptionsDialogProps> = ({
|
||||||
showSearchAdvanced,
|
showSearchAdvanced,
|
||||||
setShowSearchAdvanced,
|
setShowSearchAdvanced,
|
||||||
searchOptions,
|
searchOptions,
|
||||||
updateSearchOption
|
updateSearchOption,
|
||||||
|
folders
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={showSearchAdvanced} onOpenChange={setShowSearchAdvanced}>
|
<Dialog open={showSearchAdvanced} onOpenChange={setShowSearchAdvanced}>
|
||||||
@ -97,6 +100,29 @@ const SearchOptionsDialog: React.FC<SearchOptionsDialogProps> = ({
|
|||||||
onCheckedChange={(checked) => updateSearchOption('use_colpali', checked)}
|
onCheckedChange={(checked) => updateSearchOption('use_colpali', checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="folderName" className="block mb-2">Scope to Folder</Label>
|
||||||
|
<Select
|
||||||
|
value={searchOptions.folder_name || "__none__"}
|
||||||
|
onValueChange={(value) => updateSearchOption('folder_name', value === "__none__" ? undefined : value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full" id="folderName">
|
||||||
|
<SelectValue placeholder="Select a folder" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">All Folders</SelectItem>
|
||||||
|
{folders.map(folder => (
|
||||||
|
<SelectItem key={folder.name} value={folder.name}>
|
||||||
|
{folder.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Limit search results to documents within a specific folder
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
@ -10,7 +10,7 @@ import { showAlert } from '@/components/ui/alert-system';
|
|||||||
import SearchOptionsDialog from './SearchOptionsDialog';
|
import SearchOptionsDialog from './SearchOptionsDialog';
|
||||||
import SearchResultCard from './SearchResultCard';
|
import SearchResultCard from './SearchResultCard';
|
||||||
|
|
||||||
import { SearchResult, SearchOptions } from '@/components/types';
|
import { SearchResult, SearchOptions, Folder } from '@/components/types';
|
||||||
|
|
||||||
interface SearchSectionProps {
|
interface SearchSectionProps {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
@ -22,6 +22,7 @@ const SearchSection: React.FC<SearchSectionProps> = ({ apiBaseUrl, authToken })
|
|||||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showSearchAdvanced, setShowSearchAdvanced] = useState(false);
|
const [showSearchAdvanced, setShowSearchAdvanced] = useState(false);
|
||||||
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
const [searchOptions, setSearchOptions] = useState<SearchOptions>({
|
const [searchOptions, setSearchOptions] = useState<SearchOptions>({
|
||||||
filters: '{}',
|
filters: '{}',
|
||||||
k: 4,
|
k: 4,
|
||||||
@ -38,10 +39,32 @@ const SearchSection: React.FC<SearchSectionProps> = ({ apiBaseUrl, authToken })
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset search results when auth token or API URL changes
|
// Fetch folders and reset search results when auth token or API URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('SearchSection: Token or API URL changed, resetting results');
|
console.log('SearchSection: Token or API URL changed, resetting results');
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
|
|
||||||
|
// Fetch available folders
|
||||||
|
const fetchFolders = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/folders`, {
|
||||||
|
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const folderData = await response.json();
|
||||||
|
setFolders(folderData);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch folders', response.statusText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching folders:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authToken || apiBaseUrl.includes('localhost')) {
|
||||||
|
fetchFolders();
|
||||||
|
}
|
||||||
}, [authToken, apiBaseUrl]);
|
}, [authToken, apiBaseUrl]);
|
||||||
|
|
||||||
// Handle search
|
// Handle search
|
||||||
@ -129,6 +152,7 @@ const SearchSection: React.FC<SearchSectionProps> = ({ apiBaseUrl, authToken })
|
|||||||
setShowSearchAdvanced={setShowSearchAdvanced}
|
setShowSearchAdvanced={setShowSearchAdvanced}
|
||||||
searchOptions={searchOptions}
|
searchOptions={searchOptions}
|
||||||
updateSearchOption={updateSearchOption}
|
updateSearchOption={updateSearchOption}
|
||||||
|
folders={folders}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,18 @@ export interface Document {
|
|||||||
additional_metadata: Record<string, unknown>;
|
additional_metadata: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Folder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
owner: string;
|
||||||
|
document_ids: string[];
|
||||||
|
system_metadata: Record<string, unknown>;
|
||||||
|
access_control?: Record<string, unknown>;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
document_id: string;
|
document_id: string;
|
||||||
chunk_number: number;
|
chunk_number: number;
|
||||||
@ -39,6 +51,7 @@ export interface SearchOptions {
|
|||||||
min_score: number;
|
min_score: number;
|
||||||
use_reranking: boolean;
|
use_reranking: boolean;
|
||||||
use_colpali: boolean;
|
use_colpali: boolean;
|
||||||
|
folder_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryOptions extends SearchOptions {
|
export interface QueryOptions extends SearchOptions {
|
||||||
|
@ -245,7 +245,7 @@ export function Sidebar({
|
|||||||
onClick={() => onSectionChange("documents")}
|
onClick={() => onSectionChange("documents")}
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4" />
|
<FileText className="h-4 w-4" />
|
||||||
{!isCollapsed && <span className="ml-2">Documents</span>}
|
{!isCollapsed && <span className="ml-2">Folders</span>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -50,12 +50,15 @@
|
|||||||
"@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",
|
||||||
|
"accessor-fn": "^1.5.3",
|
||||||
"accordion": "^3.0.2",
|
"accordion": "^3.0.2",
|
||||||
"alert": "^6.0.2",
|
"alert": "^6.0.2",
|
||||||
|
"caniuse-lite": "^1.0.30001714",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"force-graph": "^1.49.4",
|
"force-graph": "^1.49.4",
|
||||||
"formdata-node": "^6.0.3",
|
"formdata-node": "^6.0.3",
|
||||||
|
"kapsule": "^1.16.3",
|
||||||
"label": "^0.2.2",
|
"label": "^0.2.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next": "^14",
|
"next": "^14",
|
||||||
@ -78,22 +81,23 @@
|
|||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.16",
|
"eslint-config-next": "14.2.16",
|
||||||
"postcss": "^8",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "^14",
|
|
||||||
"react": "^18",
|
|
||||||
"react-dom": "^18",
|
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"next": "^14",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user