mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
feat: update UI components to use Avatar for member display
- Replaced image rendering with Avatar component in PublicChatSnapshotRow, ImageModelManager, and ModelConfigManager for improved consistency. - Adjusted DocumentsFilters to modify PopoverContent width for better layout. - Enhanced DocumentsTableShell with new state management for member data and added bulk delete functionality.
This commit is contained in:
parent
f9606679e0
commit
1db716ab6d
5 changed files with 252 additions and 262 deletions
|
|
@ -76,10 +76,10 @@ export function DocumentsFilters({
|
|||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 !p-0 overflow-hidden" align="end">
|
||||
<PopoverContent className="w-56 md:w-52 !p-0 overflow-hidden" align="end">
|
||||
<div>
|
||||
{/* Search input */}
|
||||
<div className="p-2 border-b border-border dark:border-neutral-700">
|
||||
<div className="p-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-0.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -9,14 +9,17 @@ import {
|
|||
Eye,
|
||||
FileText,
|
||||
FileX,
|
||||
MoreHorizontal,
|
||||
Network,
|
||||
PenLine,
|
||||
SearchX,
|
||||
Trash2,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||
import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { toast } from "sonner";
|
||||
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||
|
|
@ -35,11 +38,11 @@ import {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
|
|
@ -48,6 +51,7 @@ import {
|
|||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
|
|
@ -61,12 +65,20 @@ import {
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useLongPress } from "@/hooks/use-long-press";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { getDocumentTypeIcon, getDocumentTypeLabel } from "./DocumentTypeIcon";
|
||||
import { getDocumentTypeIcon } from "./DocumentTypeIcon";
|
||||
import type { Document, DocumentStatus } from "./types";
|
||||
|
||||
const EDITABLE_DOCUMENT_TYPES = ["FILE", "NOTE"] as const;
|
||||
const NON_DELETABLE_DOCUMENT_TYPES = ["SURFSENSE_DOCS"] as const;
|
||||
|
||||
function getInitials(name: string): string {
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
}
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function StatusIndicator({ status }: { status?: DocumentStatus }) {
|
||||
const state = status?.state ?? "ready";
|
||||
|
||||
|
|
@ -145,45 +157,6 @@ function formatAbsoluteDate(dateStr: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
function DocumentNameTooltip({ doc, className }: { doc: Document; className?: string }) {
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkTruncation = () => {
|
||||
if (textRef.current) {
|
||||
setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth);
|
||||
}
|
||||
};
|
||||
checkTruncation();
|
||||
window.addEventListener("resize", checkTruncation);
|
||||
return () => window.removeEventListener("resize", checkTruncation);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span ref={textRef} className={className}>
|
||||
{doc.title}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" className="max-w-sm">
|
||||
<div className="space-y-1 text-xs">
|
||||
{isTruncated && <p className="font-medium text-sm break-words">{doc.title}</p>}
|
||||
<p>
|
||||
<span className="text-muted-foreground">Owner:</span>{" "}
|
||||
{doc.created_by_name || doc.created_by_email || "—"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">Created:</span>{" "}
|
||||
{formatAbsoluteDate(doc.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SortableHeader({
|
||||
children,
|
||||
sortKey,
|
||||
|
|
@ -217,71 +190,6 @@ function SortableHeader({
|
|||
);
|
||||
}
|
||||
|
||||
function RowContextMenu({
|
||||
doc,
|
||||
children,
|
||||
onPreview,
|
||||
onDelete,
|
||||
searchSpaceId,
|
||||
}: {
|
||||
doc: Document;
|
||||
children: React.ReactNode;
|
||||
onPreview: (doc: Document) => void;
|
||||
onDelete: (doc: Document) => void;
|
||||
searchSpaceId: string;
|
||||
}) {
|
||||
const openEditor = useSetAtom(openEditorPanelAtom);
|
||||
|
||||
const isEditable = EDITABLE_DOCUMENT_TYPES.includes(
|
||||
doc.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number]
|
||||
);
|
||||
const isBeingProcessed = doc.status?.state === "pending" || doc.status?.state === "processing";
|
||||
const isFileFailed = doc.document_type === "FILE" && doc.status?.state === "failed";
|
||||
const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes(
|
||||
doc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number]
|
||||
);
|
||||
const isEditDisabled = isBeingProcessed || isFileFailed;
|
||||
const isDeleteDisabled = isBeingProcessed;
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-48">
|
||||
<ContextMenuItem onClick={() => onPreview(doc)}>
|
||||
<Eye className="h-4 w-4" />
|
||||
Preview
|
||||
</ContextMenuItem>
|
||||
{isEditable && (
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
if (!isEditDisabled) {
|
||||
openEditor({
|
||||
documentId: doc.id,
|
||||
searchSpaceId: Number(searchSpaceId),
|
||||
title: doc.title,
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={isEditDisabled}
|
||||
>
|
||||
<PenLine className="h-4 w-4" />
|
||||
Edit
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{shouldShowDelete && (
|
||||
<ContextMenuItem
|
||||
onClick={() => !isDeleteDisabled && onDelete(doc)}
|
||||
disabled={isDeleteDisabled}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileCardWrapper({
|
||||
onLongPress,
|
||||
children,
|
||||
|
|
@ -370,6 +278,22 @@ export function DocumentsTableShell({
|
|||
const [bulkDeleteConfirmOpen, setBulkDeleteConfirmOpen] = useState(false);
|
||||
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
||||
const openEditor = useSetAtom(openEditorPanelAtom);
|
||||
const [openMenuDocId, setOpenMenuDocId] = useState<number | null>(null);
|
||||
|
||||
const { data: members } = useAtomValue(membersAtom);
|
||||
const memberMap = useMemo(() => {
|
||||
const map = new Map<string, { name: string; email?: string; avatarUrl?: string }>();
|
||||
if (members) {
|
||||
for (const m of members) {
|
||||
map.set(m.user_id, {
|
||||
name: m.user_display_name || m.user_email || "Unknown",
|
||||
email: m.user_email || undefined,
|
||||
avatarUrl: m.user_avatar_url || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [members]);
|
||||
|
||||
const desktopSentinelRef = useRef<HTMLDivElement>(null);
|
||||
const mobileSentinelRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -549,7 +473,20 @@ export function DocumentsTableShell({
|
|||
}, [deletableSelectedIds, bulkDeleteDocuments, deleteDocument]);
|
||||
|
||||
return (
|
||||
<div className="bg-sidebar overflow-hidden select-none border-t border-border/50 flex-1 flex flex-col min-h-0">
|
||||
<div className="bg-sidebar overflow-hidden select-none border-t border-border/50 flex-1 flex flex-col min-h-0 relative">
|
||||
{/* Floating bulk delete pill */}
|
||||
{hasDeletableSelection && (
|
||||
<div className="absolute left-1/2 -translate-x-1/2 top-2 md:top-9 z-20 animate-in fade-in slide-in-from-top-1 duration-150">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBulkDeleteConfirmOpen(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-destructive text-destructive-foreground shadow-md text-xs font-medium"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete ({deletableSelectedIds.length} selected)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Desktop Table View */}
|
||||
<div className="hidden md:flex md:flex-col flex-1 min-h-0">
|
||||
<Table className="table-fixed w-full">
|
||||
|
|
@ -581,23 +518,10 @@ export function DocumentsTableShell({
|
|||
<Network size={14} className="text-muted-foreground" />
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="w-12 text-center h-8 pl-0 pr-3">
|
||||
{hasDeletableSelection ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBulkDeleteConfirmOpen(true)}
|
||||
className="inline-flex items-center justify-center h-6 w-6 rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete {deletableSelectedIds.length} selected</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="text-xs font-medium text-muted-foreground">Status</span>
|
||||
)}
|
||||
<TableHead className="w-10 text-center h-8 px-0 pr-2">
|
||||
<span className="flex items-center justify-center">
|
||||
<User size={14} className="text-muted-foreground" />
|
||||
</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -622,7 +546,7 @@ export function DocumentsTableShell({
|
|||
<TableCell className="w-10 px-0 py-1.5 text-center">
|
||||
<Skeleton className="h-4 w-4 mx-auto rounded" />
|
||||
</TableCell>
|
||||
<TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
|
||||
<TableCell className="w-10 px-0 pr-2 py-1.5 text-center">
|
||||
<Skeleton className="h-5 w-5 mx-auto rounded-full" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -675,6 +599,17 @@ export function DocumentsTableShell({
|
|||
{sorted.map((doc) => {
|
||||
const isMentioned = mentionedDocIds?.has(doc.id) ?? false;
|
||||
const canInteract = isSelectable(doc);
|
||||
const isBeingProcessed =
|
||||
doc.status?.state === "pending" || doc.status?.state === "processing";
|
||||
const isFileFailed =
|
||||
doc.document_type === "FILE" && doc.status?.state === "failed";
|
||||
const isEditable = EDITABLE_DOCUMENT_TYPES.includes(
|
||||
doc.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number]
|
||||
);
|
||||
const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes(
|
||||
doc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number]
|
||||
);
|
||||
const isMenuOpen = openMenuDocId === doc.id;
|
||||
const handleRowToggle = () => {
|
||||
if (canInteract && onToggleChatMention) {
|
||||
onToggleChatMention(doc, isMentioned);
|
||||
|
|
@ -690,56 +625,149 @@ export function DocumentsTableShell({
|
|||
handleRowToggle();
|
||||
};
|
||||
return (
|
||||
<RowContextMenu
|
||||
<tr
|
||||
key={doc.id}
|
||||
doc={doc}
|
||||
onPreview={handleViewDocument}
|
||||
onDelete={setDeleteDoc}
|
||||
searchSpaceId={searchSpaceId}
|
||||
className={`group border-b border-border/50 transition-colors ${
|
||||
isMentioned ? "bg-primary/5 hover:bg-primary/8" : "hover:bg-muted/30"
|
||||
} ${canInteract && hasChatMode ? "cursor-pointer" : ""}`}
|
||||
onClick={handleRowClick}
|
||||
>
|
||||
<tr
|
||||
className={`border-b border-border/50 transition-colors ${
|
||||
isMentioned ? "bg-primary/5 hover:bg-primary/8" : "hover:bg-muted/30"
|
||||
} ${canInteract && hasChatMode ? "cursor-pointer" : ""}`}
|
||||
onClick={handleRowClick}
|
||||
<TableCell
|
||||
className="w-10 pl-3 pr-0 py-1.5 text-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<TableCell
|
||||
className="w-10 pl-3 pr-0 py-1.5 text-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Checkbox
|
||||
checked={isMentioned}
|
||||
onCheckedChange={() => handleRowToggle()}
|
||||
disabled={!canInteract}
|
||||
aria-label={isMentioned ? "Remove from chat" : "Add to chat"}
|
||||
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary ${!canInteract ? "opacity-40 cursor-not-allowed" : ""}`}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
{(() => {
|
||||
const state = doc.status?.state ?? "ready";
|
||||
if (state === "pending" || state === "processing") {
|
||||
return <StatusIndicator status={doc.status} />;
|
||||
}
|
||||
if (state === "failed") {
|
||||
return (
|
||||
<>
|
||||
<span className="group-hover:hidden">
|
||||
<StatusIndicator status={doc.status} />
|
||||
</span>
|
||||
<span className="hidden group-hover:inline-flex">
|
||||
<Checkbox
|
||||
checked={isMentioned}
|
||||
onCheckedChange={() => handleRowToggle()}
|
||||
aria-label={isMentioned ? "Remove from chat" : "Add to chat"}
|
||||
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Checkbox
|
||||
checked={isMentioned}
|
||||
onCheckedChange={() => handleRowToggle()}
|
||||
aria-label={isMentioned ? "Remove from chat" : "Add to chat"}
|
||||
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="px-2 py-1.5 max-w-0">
|
||||
<span className="truncate block text-sm text-foreground cursor-default">
|
||||
{doc.title}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-10 px-0 py-1.5 text-center">
|
||||
<span className="flex items-center justify-center">
|
||||
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="w-10 px-0 pr-2 py-1.5 text-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="relative flex items-center justify-center">
|
||||
{(() => {
|
||||
const member = doc.created_by_id
|
||||
? memberMap.get(doc.created_by_id)
|
||||
: null;
|
||||
const displayName =
|
||||
member?.name ||
|
||||
doc.created_by_name ||
|
||||
doc.created_by_email ||
|
||||
"Unknown";
|
||||
const avatarUrl = member?.avatarUrl;
|
||||
const email = member?.email || doc.created_by_email || displayName;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={`flex items-center justify-center transition-[visibility] ${isMenuOpen ? "invisible" : "group-hover:invisible"}`}
|
||||
>
|
||||
<Avatar className="size-5 shrink-0">
|
||||
{avatarUrl && (
|
||||
<AvatarImage src={avatarUrl} alt={displayName} />
|
||||
)}
|
||||
<AvatarFallback className="text-[9px]">
|
||||
{getInitials(displayName)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{email}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
<div
|
||||
className={`absolute inset-0 flex items-center justify-center transition-[visibility] ${isMenuOpen ? "visible" : "invisible group-hover:visible"}`}
|
||||
>
|
||||
<DropdownMenu
|
||||
onOpenChange={(open) => setOpenMenuDocId(open ? doc.id : null)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem onClick={() => handleViewDocument(doc)}>
|
||||
<Eye className="h-4 w-4" />
|
||||
Preview
|
||||
</DropdownMenuItem>
|
||||
{isEditable && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!(isBeingProcessed || isFileFailed)) {
|
||||
openEditor({
|
||||
documentId: doc.id,
|
||||
searchSpaceId: Number(searchSpaceId),
|
||||
title: doc.title,
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={isBeingProcessed || isFileFailed}
|
||||
>
|
||||
<PenLine className="h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{shouldShowDelete && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => !isBeingProcessed && setDeleteDoc(doc)}
|
||||
disabled={isBeingProcessed}
|
||||
className=""
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="px-2 py-1.5 max-w-0">
|
||||
<DocumentNameTooltip
|
||||
doc={doc}
|
||||
className="truncate block text-sm text-foreground cursor-default"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="w-10 px-0 py-1.5 text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center justify-center">
|
||||
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
{getDocumentTypeLabel(doc.document_type)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
|
||||
<StatusIndicator status={doc.status} />
|
||||
</TableCell>
|
||||
</tr>
|
||||
</RowContextMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
|
|
@ -759,10 +787,7 @@ export function DocumentsTableShell({
|
|||
<div className="flex-1 min-w-0">
|
||||
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-4 rounded shrink-0" />
|
||||
<Skeleton className="h-5 w-5 rounded-full shrink-0" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-4 rounded shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -808,25 +833,11 @@ export function DocumentsTableShell({
|
|||
ref={mobileScrollRef}
|
||||
className="md:hidden divide-y divide-border/50 flex-1 overflow-auto"
|
||||
>
|
||||
{hasDeletableSelection && (
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-muted/50 border-b border-border/50 sticky top-0 z-10">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{deletableSelectedIds.length} selected
|
||||
</span>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-7 px-2.5 text-xs"
|
||||
onClick={() => setBulkDeleteConfirmOpen(true)}
|
||||
>
|
||||
<Trash2 size={12} className="mr-1" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{sorted.map((doc) => {
|
||||
const isMentioned = mentionedDocIds?.has(doc.id) ?? false;
|
||||
const canInteract = isSelectable(doc);
|
||||
const statusState = doc.status?.state ?? "ready";
|
||||
const showCheckbox = statusState === "ready";
|
||||
const canInteract = showCheckbox;
|
||||
const handleCardClick = (e?: React.MouseEvent) => {
|
||||
if (e && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
|
@ -856,24 +867,24 @@ export function DocumentsTableShell({
|
|||
/>
|
||||
)}
|
||||
<div className="relative z-10 flex items-center gap-3 pointer-events-none">
|
||||
<span className="pointer-events-auto">
|
||||
<Checkbox
|
||||
checked={isMentioned}
|
||||
onCheckedChange={() => handleCardClick()}
|
||||
disabled={!canInteract}
|
||||
aria-label={isMentioned ? "Remove from chat" : "Add to chat"}
|
||||
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary shrink-0 ${!canInteract ? "opacity-40 cursor-not-allowed" : ""}`}
|
||||
/>
|
||||
<span className="pointer-events-auto shrink-0">
|
||||
{showCheckbox ? (
|
||||
<Checkbox
|
||||
checked={isMentioned}
|
||||
onCheckedChange={() => handleCardClick()}
|
||||
aria-label={isMentioned ? "Remove from chat" : "Add to chat"}
|
||||
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
) : (
|
||||
<StatusIndicator status={doc.status} />
|
||||
)}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="truncate block text-sm text-foreground">{doc.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="flex items-center justify-center">
|
||||
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
|
||||
</span>
|
||||
<StatusIndicator status={doc.status} />
|
||||
</div>
|
||||
<span className="flex items-center justify-center shrink-0">
|
||||
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</MobileCardWrapper>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { Check, Copy, ExternalLink, MessageSquare, Trash2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
|
@ -158,21 +158,14 @@ export function PublicChatSnapshotRow({
|
|||
<Tooltip open={isDesktop ? undefined : false}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1.5 cursor-default">
|
||||
{member.avatarUrl ? (
|
||||
<Image
|
||||
src={member.avatarUrl}
|
||||
alt={member.name}
|
||||
width={18}
|
||||
height={18}
|
||||
className="h-4.5 w-4.5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
|
||||
<span className="text-[9px] font-semibold text-primary">
|
||||
{getInitials(member.name)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Avatar className="size-4.5 shrink-0">
|
||||
{member.avatarUrl && (
|
||||
<AvatarImage src={member.avatarUrl} alt={member.name} />
|
||||
)}
|
||||
<AvatarFallback className="text-[9px]">
|
||||
{getInitials(member.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
|
||||
{member.name}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import {
|
|||
Trash2,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
createImageGenConfigMutationAtom,
|
||||
deleteImageGenConfigMutationAtom,
|
||||
|
|
@ -498,21 +498,14 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
|
|||
<Tooltip open={isDesktop ? undefined : false}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1.5 cursor-default">
|
||||
{member.avatarUrl ? (
|
||||
<Image
|
||||
src={member.avatarUrl}
|
||||
alt={member.name}
|
||||
width={18}
|
||||
height={18}
|
||||
className="h-4.5 w-4.5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
|
||||
<span className="text-[9px] font-semibold text-primary">
|
||||
{getInitials(member.name)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Avatar className="size-4.5 shrink-0">
|
||||
{member.avatarUrl && (
|
||||
<AvatarImage src={member.avatarUrl} alt={member.name} />
|
||||
)}
|
||||
<AvatarFallback className="text-[9px]">
|
||||
{getInitials(member.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
|
||||
{member.name}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import {
|
|||
Trash2,
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
|
||||
import {
|
||||
createNewLLMConfigMutationAtom,
|
||||
|
|
@ -431,21 +431,14 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
<Tooltip open={isDesktop ? undefined : false}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1.5 cursor-default">
|
||||
{member.avatarUrl ? (
|
||||
<Image
|
||||
src={member.avatarUrl}
|
||||
alt={member.name}
|
||||
width={18}
|
||||
height={18}
|
||||
className="h-4.5 w-4.5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
|
||||
<span className="text-[9px] font-semibold text-primary">
|
||||
{getInitials(member.name)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Avatar className="size-4.5 shrink-0">
|
||||
{member.avatarUrl && (
|
||||
<AvatarImage src={member.avatarUrl} alt={member.name} />
|
||||
)}
|
||||
<AvatarFallback className="text-[9px]">
|
||||
{getInitials(member.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
|
||||
{member.name}
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue