mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 09:16:22 +02:00
feat: enhance document upload and folder synchronization UI with improved processing state indicators and responsive design adjustments
This commit is contained in:
parent
530db10539
commit
bd21c2842d
7 changed files with 359 additions and 283 deletions
|
|
@ -29,6 +29,7 @@ from app.schemas import (
|
||||||
DocumentTitleSearchResponse,
|
DocumentTitleSearchResponse,
|
||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
DocumentWithChunksRead,
|
DocumentWithChunksRead,
|
||||||
|
FolderRead,
|
||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
)
|
)
|
||||||
from app.services.task_dispatcher import TaskDispatcher, get_task_dispatcher
|
from app.services.task_dispatcher import TaskDispatcher, get_task_dispatcher
|
||||||
|
|
@ -953,15 +954,13 @@ async def get_document_by_chunk_id(
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
@router.get("/documents/watched-folders", response_model=list["FolderRead"])
|
@router.get("/documents/watched-folders", response_model=list[FolderRead])
|
||||||
async def get_watched_folders(
|
async def get_watched_folders(
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user),
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""Return root folders that are marked as watched (metadata->>'watched' = 'true')."""
|
"""Return root folders that are marked as watched (metadata->>'watched' = 'true')."""
|
||||||
from app.schemas import FolderRead # noqa: F811
|
|
||||||
|
|
||||||
await check_permission(
|
await check_permission(
|
||||||
session,
|
session,
|
||||||
user,
|
user,
|
||||||
|
|
|
||||||
|
|
@ -125,29 +125,23 @@ const DocumentUploadPopupContent: FC<{
|
||||||
onPointerDownOutside={(e) => e.preventDefault()}
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
onInteractOutside={(e) => e.preventDefault()}
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||||
className="select-none max-w-4xl w-[95vw] sm:w-full h-[calc(100dvh-2rem)] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 bg-muted dark:bg-muted text-foreground [&>button]:right-3 sm:[&>button]:right-12 [&>button]:top-3 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4 sm:[&>button_svg]:size-5"
|
className="select-none max-w-2xl w-[95vw] sm:w-[640px] h-[min(460px,75dvh)] sm:h-[min(520px,80vh)] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 bg-muted dark:bg-muted text-foreground [&>button]:right-3 sm:[&>button]:right-6 [&>button]:top-3 sm:[&>button]:top-5 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4 sm:[&>button_svg]:size-5"
|
||||||
>
|
>
|
||||||
<DialogTitle className="sr-only">Upload Document</DialogTitle>
|
<DialogTitle className="sr-only">Upload Document</DialogTitle>
|
||||||
|
|
||||||
{/* Scrollable container for mobile */}
|
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto overscroll-contain">
|
<div className="flex-1 min-h-0 overflow-y-auto overscroll-contain">
|
||||||
{/* Header - scrolls with content on mobile */}
|
<div className="sticky top-0 z-20 bg-muted px-4 sm:px-6 pt-4 sm:pt-5 pb-10">
|
||||||
<div className="sticky top-0 z-20 bg-muted px-4 sm:px-12 pt-4 sm:pt-10 pb-2 sm:pb-0">
|
<div className="flex items-center gap-2 mb-1 pr-8 sm:pr-0">
|
||||||
{/* Upload header */}
|
<h2 className="text-base sm:text-lg font-semibold tracking-tight">
|
||||||
<div className="flex items-center gap-2 sm:gap-4 mb-2 sm:mb-6">
|
Upload Documents
|
||||||
<div className="flex-1 min-w-0 pr-8 sm:pr-0">
|
</h2>
|
||||||
<h2 className="text-base sm:text-2xl font-semibold tracking-tight">
|
|
||||||
Upload Documents
|
|
||||||
</h2>
|
|
||||||
<p className="text-xs sm:text-base text-muted-foreground mt-0.5 sm:mt-1 line-clamp-1 sm:line-clamp-none">
|
|
||||||
Upload and sync your documents to your search space
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs sm:text-sm text-muted-foreground line-clamp-1">
|
||||||
|
Upload and sync your documents to your search space
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
<div className="px-4 sm:px-6 pb-4 sm:pb-6">
|
||||||
<div className="px-4 sm:px-12 pb-4 sm:pb-16">
|
|
||||||
{!isLoading && !hasDocumentSummaryLLM ? (
|
{!isLoading && !hasDocumentSummaryLLM ? (
|
||||||
<Alert variant="destructive" className="mb-4 bg-muted/50 rounded-xl border-destructive/30">
|
<Alert variant="destructive" className="mb-4 bg-muted/50 rounded-xl border-destructive/30">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
|
@ -179,9 +173,6 @@ const DocumentUploadPopupContent: FC<{
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom fade shadow - hidden on very small screens */}
|
|
||||||
<div className="hidden sm:block absolute bottom-0 left-0 right-0 h-7 bg-gradient-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" />
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -195,12 +195,14 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
|
|
||||||
<span className="flex-1 min-w-0 truncate">{doc.title}</span>
|
<span className="flex-1 min-w-0 truncate">{doc.title}</span>
|
||||||
|
|
||||||
<span className="shrink-0">
|
{getDocumentTypeIcon(doc.document_type as DocumentTypeEnum, "h-3.5 w-3.5 text-muted-foreground") && (
|
||||||
{getDocumentTypeIcon(
|
<span className="shrink-0">
|
||||||
doc.document_type as DocumentTypeEnum,
|
{getDocumentTypeIcon(
|
||||||
"h-3.5 w-3.5 text-muted-foreground"
|
doc.document_type as DocumentTypeEnum,
|
||||||
)}
|
"h-3.5 w-3.5 text-muted-foreground"
|
||||||
</span>
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Eye,
|
Eye,
|
||||||
|
|
@ -30,6 +31,8 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { FolderSelectionState } from "./FolderTreeView";
|
import type { FolderSelectionState } from "./FolderTreeView";
|
||||||
|
|
||||||
|
|
@ -55,6 +58,7 @@ interface FolderNodeProps {
|
||||||
isRenaming: boolean;
|
isRenaming: boolean;
|
||||||
childCount: number;
|
childCount: number;
|
||||||
selectionState: FolderSelectionState;
|
selectionState: FolderSelectionState;
|
||||||
|
processingState: "idle" | "processing" | "failed";
|
||||||
onToggleSelect: (folderId: number, selectAll: boolean) => void;
|
onToggleSelect: (folderId: number, selectAll: boolean) => void;
|
||||||
onToggleExpand: (folderId: number) => void;
|
onToggleExpand: (folderId: number) => void;
|
||||||
onRename: (folder: FolderDisplay, newName: string) => void;
|
onRename: (folder: FolderDisplay, newName: string) => void;
|
||||||
|
|
@ -100,6 +104,7 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
isRenaming,
|
isRenaming,
|
||||||
childCount,
|
childCount,
|
||||||
selectionState,
|
selectionState,
|
||||||
|
processingState,
|
||||||
onToggleSelect,
|
onToggleSelect,
|
||||||
onToggleExpand,
|
onToggleExpand,
|
||||||
onRename,
|
onRename,
|
||||||
|
|
@ -281,14 +286,41 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Checkbox
|
{processingState !== "idle" && selectionState === "none" ? (
|
||||||
checked={
|
<>
|
||||||
selectionState === "all" ? true : selectionState === "some" ? "indeterminate" : false
|
<Tooltip>
|
||||||
}
|
<TooltipTrigger asChild>
|
||||||
onCheckedChange={handleCheckChange}
|
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center group-hover:hidden">
|
||||||
onClick={(e) => e.stopPropagation()}
|
{processingState === "processing" ? (
|
||||||
className="h-3.5 w-3.5 shrink-0"
|
<Spinner size="xs" className="text-primary" />
|
||||||
/>
|
) : (
|
||||||
|
<AlertCircle className="h-3.5 w-3.5 text-destructive" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{processingState === "processing"
|
||||||
|
? "Syncing folder contents"
|
||||||
|
: "Some files failed to process"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Checkbox
|
||||||
|
checked={false}
|
||||||
|
onCheckedChange={handleCheckChange}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="h-3.5 w-3.5 shrink-0 hidden group-hover:flex"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
selectionState === "all" ? true : selectionState === "some" ? "indeterminate" : false
|
||||||
|
}
|
||||||
|
onCheckedChange={handleCheckChange}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="h-3.5 w-3.5 shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FolderIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<FolderIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,35 @@ export function FolderTreeView({
|
||||||
return states;
|
return states;
|
||||||
}, [folders, docsByFolder, foldersByParent, mentionedDocIds]);
|
}, [folders, docsByFolder, foldersByParent, mentionedDocIds]);
|
||||||
|
|
||||||
|
const folderProcessingStates = useMemo(() => {
|
||||||
|
const states: Record<number, "idle" | "processing" | "failed"> = {};
|
||||||
|
|
||||||
|
function compute(folderId: number): { hasProcessing: boolean; hasFailed: boolean } {
|
||||||
|
const directDocs = docsByFolder[folderId] ?? [];
|
||||||
|
let hasProcessing = directDocs.some(
|
||||||
|
(d) => d.status?.state === "pending" || d.status?.state === "processing"
|
||||||
|
);
|
||||||
|
let hasFailed = directDocs.some((d) => d.status?.state === "failed");
|
||||||
|
|
||||||
|
for (const child of foldersByParent[folderId] ?? []) {
|
||||||
|
const sub = compute(child.id);
|
||||||
|
hasProcessing = hasProcessing || sub.hasProcessing;
|
||||||
|
hasFailed = hasFailed || sub.hasFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasProcessing) states[folderId] = "processing";
|
||||||
|
else if (hasFailed) states[folderId] = "failed";
|
||||||
|
else states[folderId] = "idle";
|
||||||
|
|
||||||
|
return { hasProcessing, hasFailed };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of folders) {
|
||||||
|
if (states[f.id] === undefined) compute(f.id);
|
||||||
|
}
|
||||||
|
return states;
|
||||||
|
}, [folders, docsByFolder, foldersByParent]);
|
||||||
|
|
||||||
function renderLevel(parentId: number | null, depth: number): React.ReactNode[] {
|
function renderLevel(parentId: number | null, depth: number): React.ReactNode[] {
|
||||||
const key = parentId ?? "root";
|
const key = parentId ?? "root";
|
||||||
const childFolders = (foldersByParent[key] ?? [])
|
const childFolders = (foldersByParent[key] ?? [])
|
||||||
|
|
@ -199,6 +228,7 @@ export function FolderTreeView({
|
||||||
isRenaming={renamingFolderId === f.id}
|
isRenaming={renamingFolderId === f.id}
|
||||||
childCount={folderChildCounts[f.id] ?? 0}
|
childCount={folderChildCounts[f.id] ?? 0}
|
||||||
selectionState={folderSelectionStates[f.id] ?? "none"}
|
selectionState={folderSelectionStates[f.id] ?? "none"}
|
||||||
|
processingState={folderProcessingStates[f.id] ?? "idle"}
|
||||||
onToggleSelect={onToggleFolderSelect}
|
onToggleSelect={onToggleFolderSelect}
|
||||||
onToggleExpand={onToggleExpand}
|
onToggleExpand={onToggleExpand}
|
||||||
onRename={onRenameFolder}
|
onRename={onRenameFolder}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,21 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { CheckCircle2, ChevronDown, File as FileIcon, FileType, FolderOpen, Info, Upload, X } from "lucide-react";
|
import { CheckCircle2, ChevronDown, File as FileIcon, FileType, FolderOpen, Plus, Upload, X } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||||
import { SummaryConfig } from "@/components/assistant-ui/connector-popup/components/summary-config";
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -27,7 +24,6 @@ import {
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||||
|
|
@ -36,7 +32,6 @@ import {
|
||||||
trackDocumentUploadStarted,
|
trackDocumentUploadStarted,
|
||||||
trackDocumentUploadSuccess,
|
trackDocumentUploadSuccess,
|
||||||
} from "@/lib/posthog/events";
|
} from "@/lib/posthog/events";
|
||||||
import { GridPattern } from "./GridPattern";
|
|
||||||
|
|
||||||
interface SelectedFolder {
|
interface SelectedFolder {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -128,13 +123,12 @@ interface FileWithId {
|
||||||
file: File;
|
file: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardClass = "border border-border bg-slate-400/5 dark:bg-white/5";
|
|
||||||
|
|
||||||
// Upload limits — files are sent in batches of 5 to avoid proxy timeouts
|
|
||||||
const MAX_FILES = 50;
|
const MAX_FILES = 50;
|
||||||
const MAX_TOTAL_SIZE_MB = 200;
|
const MAX_TOTAL_SIZE_MB = 200;
|
||||||
const MAX_TOTAL_SIZE_BYTES = MAX_TOTAL_SIZE_MB * 1024 * 1024;
|
const MAX_TOTAL_SIZE_BYTES = MAX_TOTAL_SIZE_MB * 1024 * 1024;
|
||||||
|
|
||||||
|
const toggleRowClass = "flex items-center justify-between rounded-lg bg-slate-400/5 dark:bg-white/5 p-3";
|
||||||
|
|
||||||
export function DocumentUploadTab({
|
export function DocumentUploadTab({
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
|
@ -198,7 +192,7 @@ export function DocumentUploadTab({
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
accept: acceptedFileTypes,
|
accept: acceptedFileTypes,
|
||||||
maxSize: 50 * 1024 * 1024, // 50MB per file
|
maxSize: 50 * 1024 * 1024,
|
||||||
noClick: isElectron,
|
noClick: isElectron,
|
||||||
disabled: files.length >= MAX_FILES,
|
disabled: files.length >= MAX_FILES,
|
||||||
});
|
});
|
||||||
|
|
@ -270,6 +264,8 @@ export function DocumentUploadTab({
|
||||||
(MAX_TOTAL_SIZE_BYTES - totalFileSize) / (1024 * 1024)
|
(MAX_TOTAL_SIZE_BYTES - totalFileSize) / (1024 * 1024)
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
|
|
||||||
|
const hasContent = files.length > 0 || selectedFolder !== null;
|
||||||
|
|
||||||
const handleAccordionChange = useCallback(
|
const handleAccordionChange = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
setAccordionValue(value);
|
setAccordionValue(value);
|
||||||
|
|
@ -307,7 +303,7 @@ export function DocumentUploadTab({
|
||||||
});
|
});
|
||||||
toast.success(`Watching folder: ${selectedFolder.name}`);
|
toast.success(`Watching folder: ${selectedFolder.name}`);
|
||||||
} else {
|
} else {
|
||||||
toast.success(`Indexing folder: ${selectedFolder.name}`);
|
toast.success(`Syncing folder: ${selectedFolder.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedFolder(null);
|
setSelectedFolder(null);
|
||||||
|
|
@ -355,139 +351,180 @@ export function DocumentUploadTab({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const renderBrowseButton = (options?: { compact?: boolean; fullWidth?: boolean }) => {
|
||||||
<div className="space-y-3 sm:space-y-6 max-w-4xl mx-auto pt-0">
|
const { compact, fullWidth } = options ?? {};
|
||||||
<Alert className="border border-border bg-slate-400/5 dark:bg-white/5">
|
if (isFileCountLimitReached) return null;
|
||||||
<Info className="h-4 w-4 shrink-0 mt-0.5" />
|
|
||||||
<AlertDescription className="text-xs sm:text-sm leading-relaxed pt-0.5">
|
|
||||||
{t("file_size_limit")}{" "}
|
|
||||||
{t("upload_limits", { maxFiles: MAX_FILES, maxSizeMB: MAX_TOTAL_SIZE_MB })}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Card className={`relative overflow-hidden ${cardClass}`}>
|
const sizeClass = compact ? "h-7" : "h-8";
|
||||||
<div className="absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)] opacity-30">
|
const widthClass = fullWidth ? "w-full" : "";
|
||||||
<GridPattern />
|
|
||||||
</div>
|
if (isElectron) {
|
||||||
<CardContent className="p-4 sm:p-10 relative z-10">
|
return (
|
||||||
<div
|
<DropdownMenu>
|
||||||
{...getRootProps()}
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
className={`flex flex-col items-center justify-center min-h-[200px] sm:min-h-[300px] border-2 border-dashed rounded-lg transition-colors ${
|
<Button variant="secondary" size="sm" className={`text-xs gap-1 ${sizeClass} ${widthClass}`}>
|
||||||
isFileCountLimitReached || isSizeLimitReached
|
Browse
|
||||||
? "border-destructive/50 bg-destructive/5 cursor-not-allowed"
|
<ChevronDown className="h-3 w-3 opacity-60" />
|
||||||
: "border-border hover:border-primary/50 cursor-pointer"
|
</Button>
|
||||||
}`}
|
</DropdownMenuTrigger>
|
||||||
>
|
<DropdownMenuContent align="center" onClick={(e) => e.stopPropagation()}>
|
||||||
<input
|
<DropdownMenuItem onClick={handleBrowseFiles}>
|
||||||
{...getInputProps()}
|
<FileIcon className="h-4 w-4 mr-2" />
|
||||||
ref={fileInputRef}
|
Files
|
||||||
className="hidden"
|
</DropdownMenuItem>
|
||||||
onClick={handleFileInputClick}
|
<DropdownMenuItem onClick={handleBrowseFolder}>
|
||||||
/>
|
<FolderOpen className="h-4 w-4 mr-2" />
|
||||||
{isFileCountLimitReached ? (
|
Folder
|
||||||
<div className="flex flex-col items-center gap-2 sm:gap-4 text-center px-4">
|
</DropdownMenuItem>
|
||||||
<Upload className="h-8 w-8 sm:h-12 sm:w-12 text-destructive/70" />
|
</DropdownMenuContent>
|
||||||
<div>
|
</DropdownMenu>
|
||||||
<p className="text-sm sm:text-lg font-medium text-destructive">
|
);
|
||||||
{t("file_limit_reached")}
|
}
|
||||||
</p>
|
|
||||||
<p className="text-xs sm:text-sm text-muted-foreground mt-1">
|
return (
|
||||||
{t("file_limit_reached_desc", { max: MAX_FILES })}
|
<Button
|
||||||
</p>
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className={`text-xs ${sizeClass} ${widthClass}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("browse_files")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 w-full mx-auto">
|
||||||
|
{/* Hidden file input for mobile browse */}
|
||||||
|
<input
|
||||||
|
{...getInputProps()}
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onClick={handleFileInputClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* MOBILE DROP ZONE */}
|
||||||
|
<div className="sm:hidden">
|
||||||
|
{hasContent ? (
|
||||||
|
!selectedFolder && !isFileCountLimitReached && (
|
||||||
|
isElectron ? (
|
||||||
|
<div className="w-full">
|
||||||
|
{renderBrowseButton({ compact: true, fullWidth: true })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
) : isDragActive ? (
|
|
||||||
<div className="flex flex-col items-center gap-2 sm:gap-4">
|
|
||||||
<Upload className="h-8 w-8 sm:h-12 sm:w-12 text-primary" />
|
|
||||||
<p className="text-sm sm:text-lg font-medium text-primary">{t("drop_files")}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 sm:gap-4">
|
|
||||||
<Upload className="h-8 w-8 sm:h-12 sm:w-12 text-muted-foreground" />
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-sm sm:text-lg font-medium">{t("drag_drop")}</p>
|
|
||||||
<p className="text-xs sm:text-sm text-muted-foreground mt-1">{t("or_browse")}</p>
|
|
||||||
</div>
|
|
||||||
{files.length > 0 && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("remaining_capacity", { files: remainingFiles, sizeMB: remainingSizeMB })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isFileCountLimitReached && (
|
|
||||||
<div className="mt-2 sm:mt-4">
|
|
||||||
{isElectron ? (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
||||||
<Button variant="secondary" size="sm" className="text-xs sm:text-sm gap-1">
|
|
||||||
{t("browse_files")}
|
|
||||||
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="center" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<DropdownMenuItem onClick={handleBrowseFiles}>
|
|
||||||
<FileIcon className="h-4 w-4 mr-2" />
|
|
||||||
Files
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={handleBrowseFolder}>
|
|
||||||
<FolderOpen className="h-4 w-4 mr-2" />
|
|
||||||
Folder
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-xs sm:text-sm"
|
className="w-full text-xs h-8 gap-1.5 border-dashed border-muted-foreground/30"
|
||||||
onClick={(e) => {
|
onClick={() => fileInputRef.current?.click()}
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t("browse_files")}
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
Add more files
|
||||||
</Button>
|
</Button>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center gap-3 py-6 px-4 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
if (!isElectron) fileInputRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Upload className="h-7 w-7 text-muted-foreground" />
|
||||||
|
<div className="text-center space-y-1">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{isElectron ? "Select files or folder" : "Tap to select files"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("file_size_limit")}{" "}
|
||||||
|
{t("upload_limits", { maxFiles: MAX_FILES, maxSizeMB: MAX_TOTAL_SIZE_MB })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{isElectron && (
|
||||||
|
<div className="w-full mt-1" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{renderBrowseButton({ fullWidth: true })}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{selectedFolder && (
|
{/* DESKTOP DROP ZONE */}
|
||||||
<Card className={cardClass}>
|
<div
|
||||||
<CardHeader className="p-4 sm:p-6">
|
{...getRootProps()}
|
||||||
<div className="flex items-center justify-between gap-2">
|
className={`hidden sm:block border-2 border-dashed rounded-lg transition-colors ${
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
isFileCountLimitReached || isSizeLimitReached
|
||||||
<FolderOpen className="h-5 w-5 text-primary flex-shrink-0" />
|
? "border-destructive/50 bg-destructive/5 cursor-not-allowed"
|
||||||
<div className="min-w-0 flex-1">
|
: "border-muted-foreground/30 hover:border-foreground/70 cursor-pointer"
|
||||||
<CardTitle className="text-base sm:text-lg truncate">
|
} ${hasContent ? "p-3" : "py-20 px-4"}`}
|
||||||
{selectedFolder.name}
|
>
|
||||||
</CardTitle>
|
{hasContent ? (
|
||||||
<CardDescription className="text-xs sm:text-sm truncate">
|
<div className="flex items-center gap-3">
|
||||||
{selectedFolder.path}
|
<Upload className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
</CardDescription>
|
<span className="text-xs text-muted-foreground flex-1 truncate">
|
||||||
</div>
|
{isDragActive
|
||||||
|
? t("drop_files")
|
||||||
|
: isFileCountLimitReached
|
||||||
|
? t("file_limit_reached")
|
||||||
|
: t("remaining_capacity", { files: remainingFiles, sizeMB: remainingSizeMB })}
|
||||||
|
</span>
|
||||||
|
{renderBrowseButton({ compact: true })}
|
||||||
|
</div>
|
||||||
|
) : isFileCountLimitReached ? (
|
||||||
|
<div className="flex flex-col items-center gap-2 text-center">
|
||||||
|
<Upload className="h-8 w-8 text-destructive/70" />
|
||||||
|
<p className="text-sm font-medium text-destructive">{t("file_limit_reached")}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("file_limit_reached_desc", { max: MAX_FILES })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : isDragActive ? (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Upload className="h-8 w-8 text-primary" />
|
||||||
|
<p className="text-sm font-medium text-primary">{t("drop_files")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Upload className="h-8 w-8 text-muted-foreground" />
|
||||||
|
<p className="text-sm font-medium">{t("drag_drop")}</p>
|
||||||
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
|
{t("file_size_limit")}{" "}
|
||||||
|
{t("upload_limits", { maxFiles: MAX_FILES, maxSizeMB: MAX_TOTAL_SIZE_MB })}
|
||||||
|
</p>
|
||||||
|
<div className="mt-1">{renderBrowseButton()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FOLDER SELECTED */}
|
||||||
|
{selectedFolder && (
|
||||||
|
<div className="rounded-lg border border-border p-3 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FolderOpen className="h-4 w-4 text-primary shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium truncate">{selectedFolder.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">{selectedFolder.path}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-7 w-7 shrink-0"
|
||||||
onClick={() => setSelectedFolder(null)}
|
onClick={() => setSelectedFolder(null)}
|
||||||
disabled={folderSubmitting}
|
disabled={folderSubmitting}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4 sm:p-6 pt-0 space-y-4">
|
<div className={toggleRowClass}>
|
||||||
<div className="flex items-center justify-between rounded-lg border border-border p-3">
|
<Label htmlFor="watch-folder-toggle" className="flex flex-col gap-0.5 cursor-pointer">
|
||||||
<Label htmlFor="watch-folder-toggle" className="flex flex-col gap-1 cursor-pointer">
|
|
||||||
<span className="text-sm font-medium">Watch folder</span>
|
<span className="text-sm font-medium">Watch folder</span>
|
||||||
<span className="text-xs text-muted-foreground font-normal">
|
<span className="text-xs text-muted-foreground font-normal">
|
||||||
Automatically sync changes when files are added, edited, or removed
|
Auto-sync when files change
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -497,150 +534,133 @@ export function DocumentUploadTab({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SummaryConfig enabled={shouldSummarize} onEnabledChange={setShouldSummarize} />
|
<div className={toggleRowClass}>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p className="font-medium text-sm">Enable AI Summary</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Improves search quality but adds latency
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={shouldSummarize} onCheckedChange={setShouldSummarize} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full py-3 sm:py-6 text-xs sm:text-base font-medium"
|
className="w-full relative"
|
||||||
onClick={handleFolderSubmit}
|
onClick={handleFolderSubmit}
|
||||||
disabled={folderSubmitting}
|
disabled={folderSubmitting}
|
||||||
>
|
>
|
||||||
{folderSubmitting ? (
|
<span className={folderSubmitting ? "invisible" : ""}>
|
||||||
<span className="flex items-center gap-2">
|
{watchFolder ? "Sync & Watch for Changes" : "Sync Folder"}
|
||||||
|
</span>
|
||||||
|
{folderSubmitting && (
|
||||||
|
<span className="absolute inset-0 flex items-center justify-center">
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
Processing...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="h-4 w-4 sm:h-5 sm:w-5" />
|
|
||||||
{watchFolder ? "Watch & Index Folder" : "Index Folder"}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{files.length > 0 && (
|
|
||||||
<Card className={cardClass}>
|
|
||||||
<CardHeader className="p-4 sm:p-6">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<CardTitle className="text-base sm:text-2xl">
|
|
||||||
{t("selected_files", { count: files.length })}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-xs sm:text-sm">
|
|
||||||
{t("total_size")}: {formatFileSize(totalFileSize)}
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-xs sm:text-sm shrink-0"
|
|
||||||
onClick={() => setFiles([])}
|
|
||||||
disabled={isUploading}
|
|
||||||
>
|
|
||||||
{t("clear_all")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4 sm:p-6 pt-0">
|
|
||||||
<div className="space-y-2 sm:space-y-3 max-h-[250px] sm:max-h-[400px] overflow-y-auto">
|
|
||||||
{files.map((entry) => (
|
|
||||||
<div
|
|
||||||
key={entry.id}
|
|
||||||
className={`flex items-center justify-between p-2 sm:p-4 rounded-lg border border-border ${cardClass} hover:bg-slate-400/10 dark:hover:bg-white/10 transition-colors`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
||||||
<FileType className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm sm:text-base font-medium truncate">{entry.file.name}</p>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{formatFileSize(entry.file.size)}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{entry.file.type || "Unknown type"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setFiles((prev) => prev.filter((e) => e.id !== entry.id))}
|
|
||||||
disabled={isUploading}
|
|
||||||
className="h-8 w-8"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isUploading && (
|
|
||||||
<div className="mt-3 sm:mt-6 space-y-2 sm:space-y-3">
|
|
||||||
<Separator className="bg-border" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between text-xs sm:text-sm">
|
|
||||||
<span>{t("uploading_files")}</span>
|
|
||||||
<span>{Math.round(uploadProgress)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={uploadProgress} className="h-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-3 sm:mt-6">
|
|
||||||
<SummaryConfig enabled={shouldSummarize} onEnabledChange={setShouldSummarize} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 sm:mt-6">
|
|
||||||
<Button
|
|
||||||
className="w-full py-3 sm:py-6 text-xs sm:text-base font-medium"
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={isUploading || files.length === 0}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<Spinner size="sm" />
|
|
||||||
{t("uploading")}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="h-4 w-4 sm:h-5 sm:w-5" />
|
|
||||||
{t("upload_button", { count: files.length })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* FILES SELECTED */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="rounded-lg border border-border p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{t("selected_files", { count: files.length })} · {formatFileSize(totalFileSize)}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => setFiles([])}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
{t("clear_all")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[160px] sm:max-h-[200px] overflow-y-auto -mx-1">
|
||||||
|
{files.map((entry) => (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
className="flex items-center gap-2 py-1.5 px-2 rounded-md hover:bg-slate-400/5 dark:hover:bg-white/5 group"
|
||||||
|
>
|
||||||
|
<FileType className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
|
<span className="text-sm truncate flex-1 min-w-0">{entry.file.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">
|
||||||
|
{formatFileSize(entry.file.size)}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 shrink-0"
|
||||||
|
onClick={() => setFiles((prev) => prev.filter((e) => e.id !== entry.id))}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isUploading && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span>{t("uploading_files")}</span>
|
||||||
|
<span>{Math.round(uploadProgress)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={uploadProgress} className="h-1.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={toggleRowClass}>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p className="font-medium text-sm">Enable AI Summary</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Improves search quality but adds latency
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={shouldSummarize} onCheckedChange={setShouldSummarize} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={isUploading || files.length === 0}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
{t("uploading")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
{t("upload_button", { count: files.length })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SUPPORTED FORMATS */}
|
||||||
<Accordion
|
<Accordion
|
||||||
type="single"
|
type="single"
|
||||||
collapsible
|
collapsible
|
||||||
value={accordionValue}
|
value={accordionValue}
|
||||||
onValueChange={handleAccordionChange}
|
onValueChange={handleAccordionChange}
|
||||||
className={`w-full ${cardClass} border border-border rounded-lg mb-0`}
|
className="w-full"
|
||||||
>
|
>
|
||||||
<AccordionItem value="supported-file-types" className="border-0">
|
<AccordionItem value="supported-file-types" className="border border-border rounded-lg">
|
||||||
<AccordionTrigger className="px-3 sm:px-6 py-3 sm:py-4 hover:no-underline !items-center [&>svg]:!translate-y-0">
|
<AccordionTrigger className="px-3 py-2.5 hover:no-underline !items-center [&>svg]:!translate-y-0">
|
||||||
<div className="flex items-center gap-2 flex-1">
|
<span className="text-xs sm:text-sm text-muted-foreground font-normal">
|
||||||
<div className="text-left min-w-0">
|
{t("supported_file_types")}
|
||||||
<div className="font-semibold text-sm sm:text-base">
|
</span>
|
||||||
{t("supported_file_types")}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs sm:text-sm text-muted-foreground font-normal">
|
|
||||||
{t("file_types_desc")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="px-3 sm:px-6 pb-3 sm:pb-6">
|
<AccordionContent className="px-3 pb-3">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-1">
|
||||||
{supportedExtensions.map((ext) => (
|
{supportedExtensions.map((ext) => (
|
||||||
<Badge key={ext} variant="outline" className="text-xs">
|
<Badge key={ext} variant="outline" className="text-[10px] px-1.5 py-0">
|
||||||
{ext}
|
{ext}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
|
||||||
return <Microscope {...iconProps} />;
|
return <Microscope {...iconProps} />;
|
||||||
case "DEEPEST":
|
case "DEEPEST":
|
||||||
return <Telescope {...iconProps} />;
|
return <Telescope {...iconProps} />;
|
||||||
|
case "LOCAL_FOLDER_FILE":
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
return <Search {...iconProps} />;
|
return <Search {...iconProps} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue