diff --git a/src/interface/web/app/common/utils.ts b/src/interface/web/app/common/utils.ts index 8bf6db84..21e53cce 100644 --- a/src/interface/web/app/common/utils.ts +++ b/src/interface/web/app/common/utils.ts @@ -94,3 +94,33 @@ export function useDebounce(value: T, delay: number): T { return debouncedValue; } + +export const formatDateTime = (isoString: string): string => { + try { + const date = new Date(isoString); + const now = new Date(); + const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000); + + // Show relative time for recent dates + if (diffInMinutes < 1) return "just now"; + if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`; + if (diffInMinutes < 120) return "1 hour ago"; + if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`; + + // For older dates, show full formatted date + const formatter = new Intl.DateTimeFormat("en-US", { + month: "long", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + timeZoneName: "short", + }); + + return formatter.format(date); + } catch (error) { + console.error("Error formatting date:", error); + return isoString; + } +}; diff --git a/src/interface/web/app/search/layout.tsx b/src/interface/web/app/search/layout.tsx index ecaa2992..cdb0976f 100644 --- a/src/interface/web/app/search/layout.tsx +++ b/src/interface/web/app/search/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import "../globals.css"; import { ContentSecurityPolicy } from "../common/layoutHelper"; +import { Toaster } from "@/components/ui/toaster"; export const metadata: Metadata = { title: "Khoj AI - Search", @@ -35,7 +36,10 @@ export default function RootLayout({ return ( - {children} + + {children} + + ); } diff --git a/src/interface/web/app/search/page.tsx b/src/interface/web/app/search/page.tsx index c4c47122..15db5b76 100644 --- a/src/interface/web/app/search/page.tsx +++ b/src/interface/web/app/search/page.tsx @@ -17,23 +17,72 @@ import { ArrowLeft, ArrowRight, FileDashed, - FileMagnifyingGlass, GithubLogo, Lightbulb, LinkSimple, MagnifyingGlass, NoteBlank, NotionLogo, + Trash, + DotsThreeVertical, + Waveform, + Plus, + Download, + Brain, + Check, + BoxArrowDown, + Funnel, } from "@phosphor-icons/react"; import { Button } from "@/components/ui/button"; import Link from "next/link"; -import { getIconFromFilename } from "../common/iconUtils"; -import { useIsMobileWidth } from "../common/utils"; +import { formatDateTime, useIsMobileWidth } from "../common/utils"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "../components/appSidebar/appSidebar"; import { Separator } from "@/components/ui/separator"; import { KhojLogoType } from "../components/logo/khojLogo"; +import { InlineLoading } from "../components/loading/loading"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { useToast } from "@/components/ui/use-toast"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +import { uploadDataForIndexing } from "../common/chatFunctions"; +import { Progress } from "@/components/ui/progress"; +import { cn } from "@/lib/utils"; interface AdditionalData { file: string; source: string; @@ -49,6 +98,12 @@ interface SearchResult { "corpus-id": string; } +interface FileObject { + file_name: string; + raw_text: string; + updated_at: string; +} + function getNoteTypeIcon(source: string) { if (source === "notion") { return ; @@ -59,24 +114,131 @@ function getNoteTypeIcon(source: string) { return ; } -const naturalLanguageSearchQueryExamples = [ - "What does the paper say about climate change?", - "Making a cappuccino at home", - "Benefits of eating mangoes", - "How to plan a wedding on a budget", - "Appointment with Dr. Makinde on 12th August", - "Class notes lecture 3 on quantum mechanics", - "Painting concepts for acrylics", - "Abstract from the paper attention is all you need", - "Climbing Everest without oxygen", - "Solving a rubik's cube in 30 seconds", - "Facts about the planet Mars", - "How to make a website using React", - "Fish at the bottom of the ocean", - "Fish farming Kenya 2021", - "How to make a cake without an oven", - "Installing a solar panel at home", -]; +interface FileCardProps { + file: FileObject; + index: number; + setSelectedFile: (file: string) => void; + setSelectedFileFullText: (file: string) => void; + handleDownload: (fileName: string, content: string) => void; + handleDelete: (fileName: string) => void; + isDeleting: boolean; + selectedFileFullText: string | null; +} + +function FileCard({ file, setSelectedFile, setSelectedFileFullText, handleDownload, handleDelete, isDeleting, selectedFileFullText }: FileCardProps) { + return ( + + + + + +
{ + setSelectedFileFullText( + '', + ); + setSelectedFile( + file.file_name, + ); + }} + > + {file.file_name + .split("/") + .pop()} +
+
+ + + +
+ {file.file_name + .split("/") + .pop()} + +
+
+
+ +

+ {!selectedFileFullText && ( + + )} + { + selectedFileFullText + } +

+
+
+
+ + + + + + + + + + +
+
+ + +

+ {file.raw_text.slice(0, 100)}... +

+
+
+ +
+ {formatDateTime(file.updated_at)} +
+
+
+ ) +} interface NoteResultProps { note: SearchResult; @@ -89,10 +251,9 @@ function Note(props: NoteResultProps) { const fileName = isFileNameURL ? note.additional.heading : note.additional.file.split("/").pop(); - const fileIcon = getIconFromFilename(fileName || ".txt", "h-4 w-4 inline mr-2"); return ( - + {getNoteTypeIcon(note.additional.source)} @@ -110,8 +271,8 @@ function Note(props: NoteResultProps) { - - {isFileNameURL ? ( + {isFileNameURL && ( + {note.additional.file} - ) : ( -
- {fileIcon} - {note.additional.file} -
- )} -
+
+ )}
); } @@ -136,15 +292,14 @@ function focusNote(note: SearchResult) { const fileName = isFileNameURL ? note.additional.heading : note.additional.file.split("/").pop(); - const fileIcon = getIconFromFilename(fileName || ".txt", "h-4 w-4 inline mr-2"); return ( - + {fileName} - - {isFileNameURL ? ( + {isFileNameURL && ( + {note.additional.file} - ) : ( -
- {fileIcon} - {note.additional.file} -
- )} -
+
+ )}
{note.entry}
@@ -167,25 +317,264 @@ function focusNote(note: SearchResult) { ); } +const UploadFiles: React.FC<{ + setUploadedFiles: (files: string[]) => void; +}> = ({ setUploadedFiles }) => { + const [isDragAndDropping, setIsDragAndDropping] = useState(false); + + const [warning, setWarning] = useState(null); + const [error, setError] = useState(null); + const [uploading, setUploading] = useState(false); + const [progressValue, setProgressValue] = useState(0); + const fileInputRef = useRef(null); + + useEffect(() => { + if (!uploading) { + setProgressValue(0); + if (!warning && !error) { + // Force close the dialog by simulating a click on the escape key + const event = new KeyboardEvent("keydown", { key: "Escape" }); + document.dispatchEvent(event); + } + } + + if (uploading) { + const interval = setInterval(() => { + setProgressValue((prev) => { + const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5 + const nextValue = prev + increment; + return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100 + }); + }, 800); + return () => clearInterval(interval); + } + }, [uploading]); + + function handleDragOver(event: React.DragEvent) { + event.preventDefault(); + setIsDragAndDropping(true); + } + + function handleDragLeave(event: React.DragEvent) { + event.preventDefault(); + setIsDragAndDropping(false); + } + + function handleDragAndDropFiles(event: React.DragEvent) { + event.preventDefault(); + setIsDragAndDropping(false); + + if (!event.dataTransfer.files) return; + + uploadFiles(event.dataTransfer.files); + } + + function openFileInput() { + if (fileInputRef && fileInputRef.current) { + fileInputRef.current.click(); + } + } + + function handleFileChange(event: React.ChangeEvent) { + if (!event.target.files) return; + + uploadFiles(event.target.files); + } + + function uploadFiles(files: FileList) { + uploadDataForIndexing(files, setWarning, setUploading, setError, setUploadedFiles); + } + + return ( + + + + + + + Build Your Knowledge Base + + Add context for your Khoj knowledge base. + Quickly search and get personalized answers from your documents. + + +
+ +
+ {uploading && ( + + )} +
+ {warning && ( +
+
+ + {warning} +
+ +
+ )} + {error && ( +
+
+ + {error} +
+ +
+ )} +
+
+ {isDragAndDropping ? ( +
+ + Drop files to upload +
+ ) : ( +
+ + Drag and drop files here +
+ )} +
+
+
+
+
+ ); +}; + +interface FileFilterComboBoxProps { + allFiles: string[]; + onChooseFile: (file: string) => void; + isMobileWidth: boolean; + explicitFile?: string; +} + +function FileFilterComboBox(props: FileFilterComboBoxProps) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState(props.explicitFile || "") + + useEffect(() => { + if (props.explicitFile) { + setValue(props.explicitFile); + } + }, [props.explicitFile]); + + return ( + + + + + + + + + No framework found. + + {props.allFiles.map((file) => ( + { + setValue(currentValue === value ? "" : currentValue) + setOpen(false) + props.onChooseFile(currentValue) + }} + > + {file} + + + ))} + + + + + + ) +} + export default function Search() { const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState(null); const [searchResultsLoading, setSearchResultsLoading] = useState(false); + const searchInputRef = useRef(null); const [focusSearchResult, setFocusSearchResult] = useState(null); - const [exampleQuery, setExampleQuery] = useState(""); - const [fileSuggestions, setFileSuggestions] = useState([]); + const [files, setFiles] = useState([]); + const [error, setError] = useState(null); + const [fileObjectsLoading, setFileObjectsLoading] = useState(true); const [allFiles, setAllFiles] = useState([]); - const [showSuggestions, setShowSuggestions] = useState(false); const searchTimeoutRef = useRef(null); + const [selectedFile, setSelectedFile] = useState(null); + const [selectedFileFilter, setSelectedFileFilter] = useState(undefined); + const [selectedFileFullText, setSelectedFileFullText] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [pageNumber, setPageNumber] = useState(0); + const [numPages, setNumPages] = useState(1); + + const { toast } = useToast(); + const isMobileWidth = useIsMobileWidth(); useEffect(() => { - setExampleQuery( - naturalLanguageSearchQueryExamples[ - Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length) - ], - ); - // Load all files once on page load fetch('/api/content/computer', { method: 'GET', @@ -193,44 +582,44 @@ export default function Search() { 'Content-Type': 'application/json', }, }) - .then(response => response.json()) - .then(data => { - setAllFiles(data); - }) - .catch(error => { - console.error('Error loading files:', error); - }); + .then(response => response.json()) + .then(data => { + setAllFiles(data); + }) + .catch(error => { + console.error('Error loading files:', error); + }); }, []); - function getFileSuggestions(query: string) { - const fileFilterMatch = query.match(/file:([^"\s]*|"[^"]*")?/); - if (!fileFilterMatch) { - setFileSuggestions([]); - setShowSuggestions(false); - return; + useEffect(() => { + // Replace the file: filter with the selected suggestion + const fileFilterMatch = searchQuery.match(/file:([^"\s]*|"[^"]*")?/); + + if (fileFilterMatch) { + const extractedFileFilter = fileFilterMatch[1]; + // Strip the double quotes + const extractedFileFilterValue = extractedFileFilter?.replace(/"/g, ""); + + if (extractedFileFilterValue) { + setSelectedFileFilter(extractedFileFilterValue); + } } + }, [searchQuery]); - const filePrefix = fileFilterMatch[1]?.replace(/^"|"$/g, '').trim() || ''; - const filteredSuggestions = allFiles - .filter(file => file.toLowerCase().includes(filePrefix.toLowerCase())) - .sort() - .slice(0, 10); - - setFileSuggestions(filteredSuggestions); - setShowSuggestions(true); - } function handleSearchInputChange(value: string) { setSearchQuery(value); + if (!value.trim()) { + setSearchResults(null); + return; + } + // Clear previous search timeout if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } - // Get file suggestions immediately - getFileSuggestions(value); - // Debounce search if (value.trim()) { searchTimeoutRef.current = setTimeout(() => { @@ -240,16 +629,18 @@ export default function Search() { } function applySuggestion(suggestion: string) { - // Replace the file: filter with the selected suggestion - const newQuery = searchQuery.replace(/file:([^"\s]*|"[^"]*")?/, `file:"${suggestion}"`); + // Append the file: filter with the selected suggestion + const newQuery = `file:"${suggestion}" ${searchQuery}`; setSearchQuery(newQuery); - setShowSuggestions(false); + searchInputRef.current?.focus(); search(); } function search() { if (searchResultsLoading || !searchQuery.trim()) return; + setSearchResultsLoading(true); + const apiUrl = `/api/search?q=${encodeURIComponent(searchQuery)}&client=web`; fetch(apiUrl, { method: "GET", @@ -267,6 +658,102 @@ export default function Search() { }); } + const fetchFiles = async (currentPageNumber: number) => { + try { + const url = `api/content/files?page=${currentPageNumber}`; + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch files"); + + const filesData = await response.json(); + const filesList = filesData.files; + const totalPages = filesData.num_pages; + + setNumPages(totalPages); + + if (Array.isArray(filesList)) { + setFiles(filesList.toSorted()); + } + + } catch (error) { + setError("Failed to load files"); + console.error("Error fetching files:", error); + } finally { + setFileObjectsLoading(false); + } + }; + + useEffect(() => { + fetchFiles(pageNumber); + }, [pageNumber]); + + const fetchSpecificFile = async (fileName: string) => { + try { + const response = await fetch(`/api/content/file?file_name=${fileName}`); + if (!response.ok) throw new Error("Failed to fetch file"); + + const file = await response.json(); + setSelectedFileFullText(file.raw_text); + } catch (error) { + setError("Failed to load file"); + console.error("Error fetching file:", error); + } + }; + + const handleDownload = (fileName: string, content: string) => { + const blob = new Blob([content], { type: "text/plain" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${fileName.split("/").pop()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + useEffect(() => { + if (selectedFile) { + fetchSpecificFile(selectedFile); + } + }, [selectedFile]); + + useEffect(() => { + fetchFiles(pageNumber); + }, []); + + useEffect(() => { + if (uploadedFiles.length > 0) { + setPageNumber(0); + fetchFiles(0); + } + }, [uploadedFiles]); + + const handleDelete = async (fileName: string) => { + setIsDeleting(true); + try { + const response = await fetch(`/api/content/file?filename=${fileName}`, { + method: "DELETE", + }); + if (!response.ok) throw new Error("Failed to delete file"); + toast({ + title: "File deleted", + description: `File ${fileName} has been deleted`, + variant: "default", + }); + + // Refresh files list + fetchFiles(pageNumber); + } catch (error) { + toast({ + title: "Error deleting file", + description: `Failed to delete file ${fileName}`, + variant: "destructive", + }); + } finally { + setIsDeleting(false); + } + }; + return ( @@ -279,54 +766,54 @@ export default function Search() { ) : ( -

Search

+

Search Your Knowledge Base

)}
-
-
-
-
+
+
+
+
handleSearchInputChange(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { - if (showSuggestions && fileSuggestions.length > 0) { - applySuggestion(fileSuggestions[0]); - } else { - search(); - } + search(); } }} + ref={searchInputRef} type="search" - placeholder="Search Documents (type 'file:' for file suggestions)" + placeholder="Search Documents" value={searchQuery} /> - {showSuggestions && fileSuggestions.length > 0 && ( -
- {fileSuggestions.map((suggestion, index) => ( -
applySuggestion(suggestion)} - > - {suggestion} -
- ))} -
- )}
- +
+ applySuggestion(file)} isMobileWidth={isMobileWidth} explicitFile={selectedFileFilter} /> + +
+ {searchResults === null && ( + + )} + {searchResultsLoading && ( +
+ +
+ )} {focusSearchResult && (
{searchResults.map((result, index) => { return ( @@ -359,24 +855,104 @@ export default function Search() {
)} - {searchResults == null && ( - - - - - - - Search across your documents - - - - {exampleQuery} - - - )} + {!searchResultsLoading && + searchResults === null && + !searchQuery.trim() && ( +
+ {fileObjectsLoading && ( +
+ +
+ )} + {error &&
{error}
} + +
+ {files.map((file, index) => ( + + ))} +
+ + + + {/* Show prev button if not on first page */} + {pageNumber > 0 && ( + + setPageNumber(pageNumber - 1)} /> + + )} + + {/* Show first page if not on first two pages */} + {pageNumber > 1 && ( + + setPageNumber(0)}>1 + + )} + + {/* Show ellipsis if there's a gap */} + {pageNumber > 2 && ( + + + + )} + + {/* Show previous page if not on first page */} + {pageNumber > 0 && ( + + setPageNumber(pageNumber - 1)}>{pageNumber} + + )} + + {/* Current page */} + + {pageNumber + 1} + + + {/* Show next page if not on last page */} + {pageNumber < numPages - 1 && ( + + setPageNumber(pageNumber + 1)}>{pageNumber + 2} + + )} + + {/* Show ellipsis if there's a gap before last page */} + {pageNumber < numPages - 3 && ( + + + + )} + + {/* Show last page if not on last two pages */} + {pageNumber < numPages - 2 && ( + + setPageNumber(numPages - 1)}>{numPages} + + )} + + {/* Show next button if not on last page */} + {pageNumber < numPages - 1 && ( + + setPageNumber(pageNumber + 1)} /> + + )} + + + +
+ )} {searchResults && searchResults.length === 0 && ( diff --git a/src/interface/web/app/settings/page.tsx b/src/interface/web/app/settings/page.tsx index 38481ad5..9bb03834 100644 --- a/src/interface/web/app/settings/page.tsx +++ b/src/interface/web/app/settings/page.tsx @@ -3,7 +3,7 @@ import styles from "./settings.module.css"; import "intl-tel-input/styles"; -import { Suspense, useEffect, useRef, useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import { useToast } from "@/components/ui/use-toast"; import { useUserConfig, ModelOptions, UserConfig, SubscriptionStates } from "../common/auth"; @@ -23,14 +23,6 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; -import { - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandDialog, -} from "@/components/ui/command"; import { ArrowRight, @@ -56,9 +48,10 @@ import { ArrowCircleUp, ArrowCircleDown, ArrowsClockwise, - Check, CaretDown, Waveform, + MagnifyingGlass, + Brain, EyeSlash, Eye, } from "@phosphor-icons/react"; @@ -66,312 +59,11 @@ import { import Loading from "../components/loading/loading"; import IntlTelInput from "intl-tel-input/react"; -import { uploadDataForIndexing } from "../common/chatFunctions"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogContent, - AlertDialogDescription, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { Progress } from "@/components/ui/progress"; -import Link from "next/link"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "../components/appSidebar/appSidebar"; import { Separator } from "@/components/ui/separator"; import { KhojLogoType } from "../components/logo/khojLogo"; -const ManageFilesModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { - const [syncedFiles, setSyncedFiles] = useState([]); - const [selectedFiles, setSelectedFiles] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [isDragAndDropping, setIsDragAndDropping] = useState(false); - - const [warning, setWarning] = useState(null); - const [error, setError] = useState(null); - const [uploading, setUploading] = useState(false); - const [progressValue, setProgressValue] = useState(0); - const [uploadedFiles, setUploadedFiles] = useState([]); - const fileInputRef = useRef(null); - - useEffect(() => { - if (!uploading) { - setProgressValue(0); - } - - if (uploading) { - const interval = setInterval(() => { - setProgressValue((prev) => { - const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5 - const nextValue = prev + increment; - return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100 - }); - }, 800); - return () => clearInterval(interval); - } - }, [uploading]); - - useEffect(() => { - const fetchFiles = async () => { - try { - const response = await fetch("/api/content/computer"); - if (!response.ok) throw new Error("Failed to fetch files"); - - // Extract resonse - const syncedFiles = await response.json(); - // Validate response - if (Array.isArray(syncedFiles)) { - // Set synced files state - setSyncedFiles(syncedFiles.toSorted()); - } else { - console.error("Unexpected data format from API"); - } - } catch (error) { - console.error("Error fetching files:", error); - } - }; - - fetchFiles(); - }, [uploadedFiles]); - - const filteredFiles = syncedFiles.filter((file) => - file.toLowerCase().includes(searchQuery.toLowerCase()), - ); - - const deleteSelected = async () => { - let filesToDelete = selectedFiles.length > 0 ? selectedFiles : filteredFiles; - - if (filesToDelete.length === 0) { - return; - } - - try { - const response = await fetch("/api/content/files", { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ files: filesToDelete }), - }); - - if (!response.ok) throw new Error("Failed to delete files"); - - // Update the syncedFiles state - setSyncedFiles((prevFiles) => - prevFiles.filter((file) => !filesToDelete.includes(file)), - ); - - // Reset selectedFiles - setSelectedFiles([]); - } catch (error) { - console.error("Error deleting files:", error); - } - }; - - const deleteFile = async (filename: string) => { - try { - const response = await fetch( - `/api/content/file?filename=${encodeURIComponent(filename)}`, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }, - ); - - if (!response.ok) throw new Error("Failed to delete file"); - - // Update the syncedFiles state - setSyncedFiles((prevFiles) => prevFiles.filter((file) => file !== filename)); - - // Remove the file from selectedFiles if it's there - setSelectedFiles((prevSelected) => prevSelected.filter((file) => file !== filename)); - } catch (error) { - console.error("Error deleting file:", error); - } - }; - - function handleDragOver(event: React.DragEvent) { - event.preventDefault(); - setIsDragAndDropping(true); - } - - function handleDragLeave(event: React.DragEvent) { - event.preventDefault(); - setIsDragAndDropping(false); - } - - function handleDragAndDropFiles(event: React.DragEvent) { - event.preventDefault(); - setIsDragAndDropping(false); - - if (!event.dataTransfer.files) return; - - uploadFiles(event.dataTransfer.files); - } - - function openFileInput() { - if (fileInputRef && fileInputRef.current) { - fileInputRef.current.click(); - } - } - - function handleFileChange(event: React.ChangeEvent) { - if (!event.target.files) return; - - uploadFiles(event.target.files); - } - - function uploadFiles(files: FileList) { - uploadDataForIndexing(files, setWarning, setUploading, setError, setUploadedFiles); - } - - return ( - - - - - Alert - - {warning || error} - { - setWarning(null); - setError(null); - setUploading(false); - }} - > - Close - - - -
- -
- Upload files - {uploading && ( - - )} -
-
-
- {isDragAndDropping ? ( -
- - Drop files to upload -
- ) : ( -
- - Drag and drop files here -
- )} -
-
-
-
-
- -
-
- - - {syncedFiles.length === 0 ? ( -
- - No files synced -
- ) : ( -
- Could not find a good match. - - Need advanced search? Click here. - -
- )} -
- - {filteredFiles.map((filename: string) => ( - { - setSelectedFiles((prev) => - prev.includes(value) - ? prev.filter((f) => f !== value) - : [...prev, value], - ); - }} - > -
-
- {selectedFiles.includes(filename) && ( - - )} - {filename} -
- -
-
- ))} -
-
-
- -
-
- -
-
-
-
- ); -}; - interface DropdownComponentProps { items: ModelOptions[]; selected: number; @@ -604,7 +296,6 @@ export default function SettingsView() { const [numberValidationState, setNumberValidationState] = useState( PhoneNumberValidationState.Verified, ); - const [isManageFilesModalOpen, setIsManageFilesModalOpen] = useState(false); const { toast } = useToast(); const isMobileWidth = useIsMobileWidth(); @@ -1162,18 +853,13 @@ export default function SettingsView() {
- {isManageFilesModalOpen && ( - setIsManageFilesModalOpen(false)} - /> - )}
Content
- - - Files + + + Knowledge Base {userConfig.enabled_content_source.computer && ( - Manage your synced files + Manage and search through your digital brain. diff --git a/src/interface/web/components/ui/pagination.tsx b/src/interface/web/components/ui/pagination.tsx new file mode 100644 index 00000000..c5c99bea --- /dev/null +++ b/src/interface/web/components/ui/pagination.tsx @@ -0,0 +1,118 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" +import { ButtonProps, buttonVariants } from "@/components/ui/button" + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +