mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat: improve chat renaming functionality with dialog support in sidebar components
This commit is contained in:
parent
f3652ad7cf
commit
f0d170a595
12 changed files with 213 additions and 23 deletions
|
|
@ -3,12 +3,12 @@
|
|||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
CircleAlert,
|
||||
FilePlus2,
|
||||
FileType,
|
||||
ListFilter,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
Trash,
|
||||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
|
@ -96,7 +96,7 @@ export function DocumentsFilters({
|
|||
size="sm"
|
||||
className="h-9 gap-2 bg-white text-gray-700 border-white hover:bg-gray-50 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100"
|
||||
>
|
||||
<FilePlus2 size={16} />
|
||||
<Upload size={16} />
|
||||
<span>Upload documents</span>
|
||||
</Button>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -129,9 +129,6 @@ const DocumentUploadPopupContent: FC<{
|
|||
<div className="sticky top-0 z-20 bg-muted px-4 sm:px-12 pt-4 sm:pt-10 pb-2 sm:pb-0">
|
||||
{/* Upload header */}
|
||||
<div className="flex items-center gap-2 sm:gap-4 mb-2 sm:mb-6">
|
||||
<div className="flex h-9 w-9 sm:h-14 sm:w-14 items-center justify-center rounded-lg sm:rounded-xl bg-primary/10 border border-primary/20 flex-shrink-0">
|
||||
<Upload className="size-4 sm:size-7 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 pr-8 sm:pr-0">
|
||||
<h2 className="text-base sm:text-2xl font-semibold tracking-tight">
|
||||
Upload Documents
|
||||
|
|
|
|||
|
|
@ -703,7 +703,6 @@ export function LayoutDataProvider({
|
|||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<PencilIcon className="h-5 w-5" />
|
||||
<span>{tSidebar("rename_chat") || "Rename Chat"}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
|
@ -736,7 +735,7 @@ export function LayoutDataProvider({
|
|||
{isRenamingChat ? (
|
||||
<>
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
{tSidebar("renaming") || "Renaming..."}
|
||||
{tSidebar("renaming") || "Renaming"}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
ArchiveIcon,
|
||||
MessageCircleMore,
|
||||
MoreHorizontal,
|
||||
PenLine,
|
||||
RotateCcwIcon,
|
||||
Search,
|
||||
Trash2,
|
||||
|
|
@ -17,6 +18,14 @@ import { useTranslations } from "next-intl";
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -69,6 +78,10 @@ export function AllPrivateChatsSidebar({
|
|||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||
const [showRenameDialog, setShowRenameDialog] = useState(false);
|
||||
const [renamingThread, setRenamingThread] = useState<{ id: number; title: string } | null>(null);
|
||||
const [newTitle, setNewTitle] = useState("");
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||
|
|
@ -187,6 +200,35 @@ export function AllPrivateChatsSidebar({
|
|||
[queryClient, searchSpaceId, t]
|
||||
);
|
||||
|
||||
const handleStartRename = useCallback((threadId: number, title: string) => {
|
||||
setRenamingThread({ id: threadId, title });
|
||||
setNewTitle(title);
|
||||
setShowRenameDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmRename = useCallback(async () => {
|
||||
if (!renamingThread || !newTitle.trim()) return;
|
||||
setIsRenaming(true);
|
||||
try {
|
||||
await updateThread(renamingThread.id, { title: newTitle.trim() });
|
||||
toast.success(t("chat_renamed") || "Chat renamed");
|
||||
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["threads", searchSpaceId, "detail", String(renamingThread.id)],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error renaming thread:", error);
|
||||
toast.error(t("error_renaming_chat") || "Failed to rename chat");
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
setShowRenameDialog(false);
|
||||
setRenamingThread(null);
|
||||
setNewTitle("");
|
||||
}
|
||||
}, [renamingThread, newTitle, queryClient, searchSpaceId, t]);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchQuery("");
|
||||
}, []);
|
||||
|
|
@ -355,11 +397,19 @@ export function AllPrivateChatsSidebar({
|
|||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
{!thread.archived && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
{thread.archived ? (
|
||||
<>
|
||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||
|
|
@ -412,6 +462,51 @@ export function AllPrivateChatsSidebar({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Dialog open={showRenameDialog} onOpenChange={setShowRenameDialog}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span>{t("rename_chat") || "Rename Chat"}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("rename_chat_description") || "Enter a new name for this conversation."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
placeholder={t("chat_title_placeholder") || "Chat title"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !isRenaming && newTitle.trim()) {
|
||||
handleConfirmRename();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex gap-2 sm:justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowRenameDialog(false)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmRename}
|
||||
disabled={isRenaming || !newTitle.trim()}
|
||||
className="gap-2"
|
||||
>
|
||||
{isRenaming ? (
|
||||
<>
|
||||
<Spinner size="xs" />
|
||||
<span>{t("renaming") || "Renaming"}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SidebarSlideOutPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
ArchiveIcon,
|
||||
MessageCircleMore,
|
||||
MoreHorizontal,
|
||||
PenLine,
|
||||
RotateCcwIcon,
|
||||
Search,
|
||||
Trash2,
|
||||
|
|
@ -17,6 +18,14 @@ import { useTranslations } from "next-intl";
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -69,6 +78,10 @@ export function AllSharedChatsSidebar({
|
|||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||
const [showRenameDialog, setShowRenameDialog] = useState(false);
|
||||
const [renamingThread, setRenamingThread] = useState<{ id: number; title: string } | null>(null);
|
||||
const [newTitle, setNewTitle] = useState("");
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||
|
|
@ -187,6 +200,35 @@ export function AllSharedChatsSidebar({
|
|||
[queryClient, searchSpaceId, t]
|
||||
);
|
||||
|
||||
const handleStartRename = useCallback((threadId: number, title: string) => {
|
||||
setRenamingThread({ id: threadId, title });
|
||||
setNewTitle(title);
|
||||
setShowRenameDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmRename = useCallback(async () => {
|
||||
if (!renamingThread || !newTitle.trim()) return;
|
||||
setIsRenaming(true);
|
||||
try {
|
||||
await updateThread(renamingThread.id, { title: newTitle.trim() });
|
||||
toast.success(t("chat_renamed") || "Chat renamed");
|
||||
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["threads", searchSpaceId, "detail", String(renamingThread.id)],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error renaming thread:", error);
|
||||
toast.error(t("error_renaming_chat") || "Failed to rename chat");
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
setShowRenameDialog(false);
|
||||
setRenamingThread(null);
|
||||
setNewTitle("");
|
||||
}
|
||||
}, [renamingThread, newTitle, queryClient, searchSpaceId, t]);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchQuery("");
|
||||
}, []);
|
||||
|
|
@ -355,12 +397,20 @@ export function AllSharedChatsSidebar({
|
|||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
{!thread.archived && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
||||
>
|
||||
{thread.archived ? (
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
{thread.archived ? (
|
||||
<>
|
||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t("unarchive") || "Restore"}</span>
|
||||
|
|
@ -412,6 +462,51 @@ export function AllSharedChatsSidebar({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Dialog open={showRenameDialog} onOpenChange={setShowRenameDialog}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span>{t("rename_chat") || "Rename Chat"}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("rename_chat_description") || "Enter a new name for this conversation."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
placeholder={t("chat_title_placeholder") || "Chat title"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !isRenaming && newTitle.trim()) {
|
||||
handleConfirmRename();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex gap-2 sm:justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowRenameDialog(false)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmRename}
|
||||
disabled={isRenaming || !newTitle.trim()}
|
||||
className="gap-2"
|
||||
>
|
||||
{isRenaming ? (
|
||||
<>
|
||||
<Spinner size="xs" />
|
||||
<span>{t("renaming") || "Renaming"}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SidebarSlideOutPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function SidebarSlideOutPanel({
|
|||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||
className={cn(
|
||||
"h-full w-full bg-background flex flex-col pointer-events-auto",
|
||||
"h-full w-full bg-background flex flex-col pointer-events-auto select-none",
|
||||
"sm:border-r sm:shadow-xl"
|
||||
)}
|
||||
role="dialog"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useAtom } from "jotai";
|
||||
import { CheckCircle2, FileType, Info, Tag, Upload, X } from "lucide-react";
|
||||
import { CheckCircle2, FileType, Info, Upload, X } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
|
@ -452,7 +452,6 @@ export function DocumentUploadTab({
|
|||
<AccordionItem value="supported-file-types" className="border-0">
|
||||
<AccordionTrigger className="px-3 sm:px-6 py-3 sm:py-4 hover:no-underline !items-center [&>svg]:!translate-y-0">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Tag className="h-4 w-4 sm:h-5 sm:w-5 shrink-0" />
|
||||
<div className="text-left min-w-0">
|
||||
<div className="font-semibold text-sm sm:text-base">
|
||||
{t("supported_file_types")}
|
||||
|
|
|
|||
|
|
@ -682,7 +682,7 @@
|
|||
"rename_chat": "Rename Chat",
|
||||
"rename_chat_description": "Enter a new name for this conversation.",
|
||||
"chat_title_placeholder": "Chat title",
|
||||
"renaming": "Renaming...",
|
||||
"renaming": "Renaming",
|
||||
"no_archived_chats": "No archived chats",
|
||||
"error_archiving_chat": "Failed to archive chat",
|
||||
"new_chat": "New chat",
|
||||
|
|
@ -720,7 +720,8 @@
|
|||
"unread": "Unread",
|
||||
"connectors": "Connectors",
|
||||
"all_connectors": "All connectors",
|
||||
"close": "Close"
|
||||
"close": "Close",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "Something went wrong",
|
||||
|
|
|
|||
|
|
@ -720,7 +720,8 @@
|
|||
"unread": "No leído",
|
||||
"connectors": "Conectores",
|
||||
"all_connectors": "Todos los conectores",
|
||||
"close": "Cerrar"
|
||||
"close": "Cerrar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "Algo salió mal",
|
||||
|
|
|
|||
|
|
@ -720,7 +720,8 @@
|
|||
"unread": "अपठित",
|
||||
"connectors": "कनेक्टर",
|
||||
"all_connectors": "सभी कनेक्टर",
|
||||
"close": "बंद करें"
|
||||
"close": "बंद करें",
|
||||
"cancel": "रद्द करें"
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "कुछ गलत हो गया",
|
||||
|
|
|
|||
|
|
@ -720,7 +720,8 @@
|
|||
"unread": "Não lido",
|
||||
"connectors": "Conectores",
|
||||
"all_connectors": "Todos os conectores",
|
||||
"close": "Fechar"
|
||||
"close": "Fechar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "Algo deu errado",
|
||||
|
|
|
|||
|
|
@ -704,7 +704,8 @@
|
|||
"unread": "未读",
|
||||
"connectors": "连接器",
|
||||
"all_connectors": "所有连接器",
|
||||
"close": "关闭"
|
||||
"close": "关闭",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "出错了",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue