Enhance alert system: add warning type and improve event handling; add Checkbox component and update dependencies

This commit is contained in:
Adityavardhan Agrawal 2025-04-10 12:15:43 -07:00
parent e6ae047aad
commit 05d5fab228
5 changed files with 272 additions and 78 deletions

View File

@ -56,10 +56,10 @@ class ColpaliEmbeddingModel(BaseEmbeddingModel):
else: else:
contents.append(chunk.content) contents.append(chunk.content)
return [self.generate_embeddings(content) for content in contents] return [await self.generate_embeddings(content) for content in contents]
async def embed_for_query(self, text: str) -> torch.Tensor: async def embed_for_query(self, text: str) -> torch.Tensor:
return self.generate_embeddings(text) return await self.generate_embeddings(text)
async def generate_embeddings(self, content: str | Image) -> np.ndarray: async def generate_embeddings(self, content: str | Image) -> np.ndarray:
if isinstance(content, Image): if isinstance(content, Image):

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useState, useEffect, ChangeEvent } from 'react'; import React, { useState, useEffect, ChangeEvent } from 'react';
import { Checkbox } from "@/components/ui/checkbox";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@ -59,15 +60,17 @@ interface QueryOptions extends SearchOptions {
graph_name?: string; graph_name?: string;
} }
interface BatchUploadError { // Commented out as currently unused
filename: string; // interface BatchUploadError {
error: string; // filename: string;
} // error: string;
// }
const MorphikUI = () => { const MorphikUI = () => {
const [activeSection, setActiveSection] = useState('documents'); const [activeSection, setActiveSection] = useState('documents');
const [documents, setDocuments] = useState<Document[]>([]); const [documents, setDocuments] = useState<Document[]>([]);
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null); const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]); const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [chatQuery, setChatQuery] = useState(''); const [chatQuery, setChatQuery] = useState('');
@ -249,6 +252,108 @@ const MorphikUI = () => {
setLoading(false); setLoading(false);
} }
}; };
// Handle multiple document deletion
const handleDeleteMultipleDocuments = async () => {
if (selectedDocuments.length === 0) return;
try {
setLoading(true);
// Show initial alert for deletion progress
const alertId = 'delete-multiple-progress';
showAlert(`Deleting ${selectedDocuments.length} documents...`, {
type: 'info',
dismissible: false,
id: alertId
});
// Perform deletions sequentially
const results = await Promise.all(
selectedDocuments.map(docId =>
fetch(`${API_BASE_URL}/documents/${docId}`, {
method: 'DELETE',
headers
})
)
);
// Check if any deletion failed
const failedCount = results.filter(res => !res.ok).length;
// Clear selected document if it was among deleted ones
if (selectedDocument && selectedDocuments.includes(selectedDocument.external_id)) {
setSelectedDocument(null);
}
// Clear selection
setSelectedDocuments([]);
// Refresh documents list
await fetchDocuments();
// Remove progress alert
removeAlert(alertId);
// Show final result alert
if (failedCount > 0) {
showAlert(`Deleted ${selectedDocuments.length - failedCount} documents. ${failedCount} deletions failed.`, {
type: "warning",
duration: 4000
});
} else {
showAlert(`Successfully deleted ${selectedDocuments.length} documents`, {
type: "success",
duration: 3000
});
}
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'An unknown error occurred';
showAlert(errorMsg, {
type: 'error',
title: 'Delete Failed',
duration: 5000
});
} finally {
setLoading(false);
}
};
// Toggle document selection - currently handled by handleCheckboxChange
// Keeping implementation in comments for reference
/*
const toggleDocumentSelection = (e: React.MouseEvent, docId: string) => {
e.stopPropagation(); // Prevent document selection/details view
setSelectedDocuments(prev => {
if (prev.includes(docId)) {
return prev.filter(id => id !== docId);
} else {
return [...prev, docId];
}
});
};
*/
// Handle checkbox change (wrapper function for use with shadcn checkbox)
const handleCheckboxChange = (checked: boolean | "indeterminate", docId: string) => {
setSelectedDocuments(prev => {
if (checked === true && !prev.includes(docId)) {
return [...prev, docId];
} else if (checked === false && prev.includes(docId)) {
return prev.filter(id => id !== docId);
}
return prev;
});
};
// Helper function to get "indeterminate" state for select all checkbox
const getSelectAllState = () => {
if (selectedDocuments.length === 0) return false;
if (selectedDocuments.length === documents.length) return true;
return "indeterminate";
};
// Handle file upload // Handle file upload
const handleFileUpload = async () => { const handleFileUpload = async () => {
@ -780,9 +885,21 @@ const MorphikUI = () => {
{activeSection === 'documents' && ( {activeSection === 'documents' && (
<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 bg-white py-3 mb-4"> <div className="flex justify-between items-center bg-white py-3 mb-4">
<div> <div className="flex items-center gap-4">
<h2 className="text-2xl font-bold leading-tight">Your Documents</h2> <div>
<p className="text-muted-foreground">Manage your uploaded documents and view their metadata.</p> <h2 className="text-2xl font-bold leading-tight">Your Documents</h2>
<p className="text-muted-foreground">Manage your uploaded documents and view their metadata.</p>
</div>
{selectedDocuments.length > 0 && (
<Button
variant="outline"
onClick={handleDeleteMultipleDocuments}
disabled={loading}
className="border-red-500 text-red-500 hover:bg-red-50 ml-4"
>
Delete {selectedDocuments.length} selected
</Button>
)}
</div> </div>
<Dialog <Dialog
open={showUploadDialog} open={showUploadDialog}
@ -939,7 +1056,21 @@ const MorphikUI = () => {
<div className="border rounded-md"> <div className="border rounded-md">
<div className="bg-gray-100 border-b p-3 font-medium sticky top-0"> <div className="bg-gray-100 border-b p-3 font-medium sticky top-0">
<div className="grid grid-cols-12"> <div className="grid grid-cols-12">
<div className="col-span-5">Filename</div> <div className="col-span-1 flex items-center justify-center">
<Checkbox
id="select-all-documents"
checked={getSelectAllState()}
onCheckedChange={(checked) => {
if (checked) {
setSelectedDocuments(documents.map(doc => doc.external_id));
} else {
setSelectedDocuments([]);
}
}}
aria-label="Select all documents"
/>
</div>
<div className="col-span-4">Filename</div>
<div className="col-span-3">Type</div> <div className="col-span-3">Type</div>
<div className="col-span-4">ID</div> <div className="col-span-4">ID</div>
</div> </div>
@ -952,7 +1083,16 @@ const MorphikUI = () => {
onClick={() => handleDocumentClick(doc)} onClick={() => handleDocumentClick(doc)}
className="grid grid-cols-12 p-3 cursor-pointer hover:bg-gray-50 border-b" className="grid grid-cols-12 p-3 cursor-pointer hover:bg-gray-50 border-b"
> >
<div className="col-span-5 flex items-center"> <div className="col-span-1 flex items-center justify-center">
<Checkbox
id={`doc-${doc.external_id}`}
checked={selectedDocuments.includes(doc.external_id)}
onCheckedChange={(checked) => handleCheckboxChange(checked, doc.external_id)}
onClick={(e) => e.stopPropagation()}
aria-label={`Select ${doc.filename || 'document'}`}
/>
</div>
<div className="col-span-4 flex items-center">
{doc.filename || 'N/A'} {doc.filename || 'N/A'}
{doc.external_id === selectedDocument?.external_id && ( {doc.external_id === selectedDocument?.external_id && (
<Badge variant="outline" className="ml-2">Selected</Badge> <Badge variant="outline" className="ml-2">Selected</Badge>
@ -1083,14 +1223,14 @@ const MorphikUI = () => {
{/* Search Section */} {/* Search Section */}
{activeSection === 'search' && ( {activeSection === 'search' && (
<Card> <Card className="flex-1 flex flex-col h-full">
<CardHeader> <CardHeader>
<CardTitle>Search Documents</CardTitle> <CardTitle>Search Documents</CardTitle>
<CardDescription> <CardDescription>
Search across your documents to find relevant information. Search across your documents to find relevant information.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex-1 flex flex-col">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
@ -1108,19 +1248,22 @@ const MorphikUI = () => {
</div> </div>
<div> <div>
<button <Dialog open={showSearchAdvanced} onOpenChange={setShowSearchAdvanced}>
type="button" <DialogTrigger asChild>
className="flex items-center text-sm text-gray-600 hover:text-gray-900" <Button variant="outline" size="sm" className="flex items-center">
onClick={() => setShowSearchAdvanced(!showSearchAdvanced)} <Settings className="mr-2 h-4 w-4" />
> Advanced Options
<Settings className="mr-1 h-4 w-4" /> </Button>
Advanced Options </DialogTrigger>
{showSearchAdvanced ? <ChevronUp className="ml-1 h-4 w-4" /> : <ChevronDown className="ml-1 h-4 w-4" />} <DialogContent className="sm:max-w-md">
</button> <DialogHeader>
<DialogTitle>Search Options</DialogTitle>
{showSearchAdvanced && ( <DialogDescription>
<div className="mt-3 p-4 border rounded-md bg-gray-50"> Configure advanced search parameters
<div className="space-y-4"> </DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div> <div>
<Label htmlFor="search-filters" className="block mb-2">Filters (JSON)</Label> <Label htmlFor="search-filters" className="block mb-2">Filters (JSON)</Label>
<Textarea <Textarea
@ -1178,49 +1321,57 @@ const MorphikUI = () => {
/> />
</div> </div>
</div> </div>
</div>
)} <DialogFooter>
<Button onClick={() => setShowSearchAdvanced(false)}>Apply</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
</div> </div>
<div className="mt-6"> <div className="mt-6 flex-1 overflow-hidden">
{searchResults.length > 0 ? ( {searchResults.length > 0 ? (
<div className="space-y-6"> <div>
<h3 className="text-lg font-medium">Results ({searchResults.length})</h3> <h3 className="text-lg font-medium mb-4">Results ({searchResults.length})</h3>
{searchResults.map((result) => ( <ScrollArea className="h-[calc(100vh-320px)]">
<Card key={`${result.document_id}-${result.chunk_number}`}> <div className="space-y-6 pr-4">
<CardHeader className="pb-2"> {searchResults.map((result) => (
<div className="flex justify-between items-start"> <Card key={`${result.document_id}-${result.chunk_number}`}>
<div> <CardHeader className="pb-2">
<CardTitle className="text-base"> <div className="flex justify-between items-start">
{result.filename || `Document ${result.document_id.substring(0, 8)}...`} <div>
</CardTitle> <CardTitle className="text-base">
<CardDescription> {result.filename || `Document ${result.document_id.substring(0, 8)}...`}
Chunk {result.chunk_number} Score: {result.score.toFixed(2)} </CardTitle>
</CardDescription> <CardDescription>
</div> Chunk {result.chunk_number} Score: {result.score.toFixed(2)}
<Badge variant="outline"> </CardDescription>
{result.content_type} </div>
</Badge> <Badge variant="outline">
</div> {result.content_type}
</CardHeader> </Badge>
<CardContent> </div>
{renderContent(result.content, result.content_type)} </CardHeader>
<CardContent>
<Accordion type="single" collapsible className="mt-4"> {renderContent(result.content, result.content_type)}
<AccordionItem value="metadata">
<AccordionTrigger className="text-sm">Metadata</AccordionTrigger> <Accordion type="single" collapsible className="mt-4">
<AccordionContent> <AccordionItem value="metadata">
<pre className="bg-gray-50 p-2 rounded text-xs overflow-x-auto"> <AccordionTrigger className="text-sm">Metadata</AccordionTrigger>
{JSON.stringify(result.metadata, null, 2)} <AccordionContent>
</pre> <pre className="bg-gray-50 p-2 rounded text-xs overflow-x-auto">
</AccordionContent> {JSON.stringify(result.metadata, null, 2)}
</AccordionItem> </pre>
</Accordion> </AccordionContent>
</CardContent> </AccordionItem>
</Card> </Accordion>
))} </CardContent>
</Card>
))}
</div>
</ScrollArea>
</div> </div>
) : ( ) : (
<div className="text-center py-16 border border-dashed rounded-lg"> <div className="text-center py-16 border border-dashed rounded-lg">

View File

@ -6,7 +6,7 @@ import { cn } from '@/lib/utils';
interface AlertInstanceProps { interface AlertInstanceProps {
id: string; id: string;
type: 'error' | 'success' | 'info' | 'upload'; type: 'error' | 'success' | 'info' | 'upload' | 'warning';
title?: string; title?: string;
message: string; message: string;
duration?: number; duration?: number;
@ -29,7 +29,8 @@ const AlertInstance = ({
type === 'error' && "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", type === 'error' && "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
type === 'upload' && "bg-blue-50 text-blue-700 border-blue-200", type === 'upload' && "bg-blue-50 text-blue-700 border-blue-200",
type === 'success' && "bg-green-50 text-green-700 border-green-200", type === 'success' && "bg-green-50 text-green-700 border-green-200",
type === 'info' && "bg-gray-50 text-gray-700 border-gray-200" type === 'info' && "bg-gray-50 text-gray-700 border-gray-200",
type === 'warning' && "bg-amber-50 text-amber-700 border-amber-200"
)} )}
> >
{dismissible && ( {dismissible && (
@ -57,13 +58,22 @@ export function AlertSystem({ position = 'bottom-right' }: AlertSystemProps) {
// Custom event handlers for adding and removing alerts // Custom event handlers for adding and removing alerts
useEffect(() => { useEffect(() => {
const handleAddAlert = (event: CustomEvent) => { const handleAddAlert = (event: Event) => {
const alert = event.detail; const customEvent = event as CustomEvent<{
id?: string;
type: 'error' | 'success' | 'info' | 'upload' | 'warning';
title?: string;
message: string;
duration?: number;
dismissible?: boolean;
}>;
const alert = customEvent.detail;
if (alert) { if (alert) {
const newAlert = { const newAlert: AlertInstanceProps = {
...alert, ...alert,
id: alert.id || Date.now().toString(), id: alert.id || Date.now().toString(),
dismissible: alert.dismissible !== false, dismissible: alert.dismissible !== false,
onDismiss: removeAlert,
}; };
setAlerts(prev => [...prev, newAlert]); setAlerts(prev => [...prev, newAlert]);
@ -77,20 +87,20 @@ export function AlertSystem({ position = 'bottom-right' }: AlertSystemProps) {
} }
}; };
const handleRemoveAlert = (event: CustomEvent) => { const handleRemoveAlert = (event: Event) => {
const { id } = event.detail; const customEvent = event as CustomEvent<{id: string}>;
const { id } = customEvent.detail;
if (id) { if (id) {
removeAlert(id); removeAlert(id);
} }
}; };
// Cast to any to handle CustomEvent window.addEventListener('morphik:alert', handleAddAlert as EventListener);
window.addEventListener('morphik:alert' as any, handleAddAlert); window.addEventListener('morphik:alert:remove', handleRemoveAlert as EventListener);
window.addEventListener('morphik:alert:remove' as any, handleRemoveAlert);
return () => { return () => {
window.removeEventListener('morphik:alert' as any, handleAddAlert); window.removeEventListener('morphik:alert', handleAddAlert as EventListener);
window.removeEventListener('morphik:alert:remove' as any, handleRemoveAlert); window.removeEventListener('morphik:alert:remove', handleRemoveAlert as EventListener);
}; };
}, []); }, []);
@ -125,7 +135,7 @@ export function AlertSystem({ position = 'bottom-right' }: AlertSystemProps) {
export const showAlert = ( export const showAlert = (
message: string, message: string,
options?: { options?: {
type?: 'error' | 'success' | 'info' | 'upload'; type?: 'error' | 'success' | 'info' | 'upload' | 'warning';
title?: string; title?: string;
duration?: number; // in milliseconds, none means it stays until dismissed duration?: number; // in milliseconds, none means it stays until dismissed
dismissible?: boolean; dismissible?: boolean;

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-checkbox": "^1.1.5",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.2",
@ -29,6 +30,7 @@
"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",
"label": "^0.2.2", "label": "^0.2.2",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"next": "^14.2.24", "next": "^14.2.24",
@ -40,7 +42,8 @@
"shadcn-ui": "^0.9.4", "shadcn-ui": "^0.9.4",
"sheet": "^0.2.0", "sheet": "^0.2.0",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"web-streams-polyfill": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@shadcn/ui": "^0.0.4", "@shadcn/ui": "^0.0.4",