Merge pull request #831 from AnishSarkar22/fix/ui

feat: multiple UI enhancements
This commit is contained in:
Rohan Verma 2026-02-24 00:32:11 -08:00 committed by GitHub
commit 47e6a7f29e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 2176 additions and 2209 deletions

View file

@ -221,7 +221,7 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger
</TooltipIconButton>
)}
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5">
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none">
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
@ -374,7 +374,7 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger
: "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."}
</p>
<Button asChild size="sm" variant="outline">
<Link href={`/dashboard/${searchSpaceId}/settings`}>
<Link href={`/dashboard/${searchSpaceId}/settings?section=models`}>
<Settings className="mr-2 h-4 w-4" />
Go to Settings
</Link>

View file

@ -120,7 +120,7 @@ const DocumentUploadPopupContent: FC<{
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="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 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">
<DialogContent 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 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">
<DialogTitle className="sr-only">Upload Document</DialogTitle>
{/* Scrollable container for mobile */}
@ -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
@ -156,7 +153,7 @@ const DocumentUploadPopupContent: FC<{
: "You need to configure a Document Summary LLM before uploading files. This LLM is used to process and summarize your uploaded documents."}
</p>
<Button asChild size="sm" variant="outline">
<Link href={`/dashboard/${searchSpaceId}/settings`}>
<Link href={`/dashboard/${searchSpaceId}/settings?section=models`}>
<Settings className="mr-2 h-4 w-4" />
Go to Settings
</Link>

View file

@ -18,6 +18,7 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
Dot,
DownloadIcon,
FileWarning,
Paperclip,
@ -745,7 +746,19 @@ const ComposerAction: FC<ComposerActionProps> = ({
<div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
<div className="flex items-center gap-1">
<TooltipIconButton
tooltip={isUploadingDocs ? "Uploading documents..." : "Upload and mention files"}
tooltip={
isUploadingDocs ? (
"Uploading documents..."
) : (
<div className="flex flex-col gap-0.5">
<span className="font-medium">Upload and mention files</span>
<span className="text-xs text-muted-foreground flex items-center">
Max 10 files <Dot className="size-3" /> 50 MB each
</span>
<span className="text-xs text-muted-foreground">Total upload limit: 200 MB</span>
</div>
)
}
side="bottom"
variant="ghost"
size="icon"

View file

@ -1,13 +1,13 @@
"use client";
import { Slottable } from "@radix-ui/react-slot";
import { type ComponentPropsWithRef, forwardRef } from "react";
import { type ComponentPropsWithRef, type ReactNode, forwardRef } from "react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {
tooltip: string;
tooltip: ReactNode;
side?: "top" | "bottom" | "left" | "right";
};

View file

@ -196,7 +196,7 @@ export function DashboardBreadcrumb() {
}
return (
<Breadcrumb>
<Breadcrumb className="select-none">
<BreadcrumbList>
{breadcrumbs.map((item, index) => (
<React.Fragment key={`${index}-${item.href || item.label}`}>

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4.94 4.96a9.97 9.97 0 0 1 10.835-2.182a8.7 8.7 0 0 1 2.033 1.11l-3.006 1.39C12.003 4.101 8.797 4.9 6.84 6.86c-2.564 2.565-3.146 6.954-.36 9.922l.278.284L.124 23c1.875-1.973 3.771-4.427 2.636-7.19c-1.52-3.698-.635-8.03 2.18-10.85M23.9.1c-2.264 3.174-3.184 5.389-2.197 9.64l-.007-.007c.753 3.201-.052 6.75-2.653 9.355c-3.279 3.285-8.526 4.016-12.847 1.06L9.21 18.75c2.758 1.084 5.775.607 7.943-1.564c2.169-2.17 2.655-5.332 1.566-7.963c-.207-.5-.828-.625-1.263-.304L8.59 15.472l12.7-12.77v.01z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"/></svg>

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 270 B

Before After
Before After

View file

@ -334,7 +334,7 @@ export function LayoutDataProvider({
const handleSearchSpaceSettings = useCallback(
(space: SearchSpace) => {
router.push(`/dashboard/${space.id}/settings`);
router.push(`/dashboard/${space.id}/settings?section=general`);
},
[router]
);
@ -478,7 +478,7 @@ export function LayoutDataProvider({
);
const handleSettings = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/settings`);
router.push(`/dashboard/${searchSpaceId}/settings?section=general`);
}, [router, searchSpaceId]);
const handleManageMembers = useCallback(() => {
@ -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"}
</>
) : (
<>

View file

@ -2,7 +2,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useAtomValue } from "jotai";
import { Plus, Search } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { useForm } from "react-hook-form";
@ -86,9 +85,6 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
<DialogContent className="max-w-[90vw] sm:max-w-sm p-4 sm:p-5 data-[state=open]:animate-none data-[state=closed]:animate-none">
<DialogHeader className="space-y-2 pb-2">
<div className="flex items-center gap-2 sm:gap-3">
<div className="flex h-8 w-8 sm:h-10 sm:w-10 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
<Search className="h-4 w-4 sm:h-5 sm:w-5 text-primary" />
</div>
<div className="flex-1 min-w-0">
<DialogTitle className="text-base sm:text-lg">{t("create_title")}</DialogTitle>
<DialogDescription className="text-xs sm:text-sm mt-0.5">
@ -142,20 +138,20 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
)}
/>
<DialogFooter className="flex-col sm:flex-row gap-2 pt-2 sm:pt-3">
<DialogFooter className="flex-row gap-2 pt-2 sm:pt-3">
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={isSubmitting}
className="w-full sm:w-auto h-9 sm:h-10 text-sm"
className="flex-1 sm:flex-none sm:w-auto h-8 sm:h-10 text-xs sm:text-sm"
>
{tCommon("cancel")}
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="w-full sm:w-auto h-9 sm:h-10 text-sm"
className="flex-1 sm:flex-none sm:w-auto h-8 sm:h-10 text-xs sm:text-sm"
>
{isSubmitting ? (
<>
@ -163,10 +159,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
{t("creating")}
</>
) : (
<>
<Plus className="-mr-1 h-4 w-4" />
{t("create_button")}
</>
<>{t("create_button")}</>
)}
</Button>
</DialogFooter>

View file

@ -210,6 +210,26 @@ export function LayoutShell({
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
/>
)}
{/* Mobile All Shared Chats - slide-out panel */}
{allSharedChatsPanel && (
<AllSharedChatsSidebar
open={allSharedChatsPanel.open}
onOpenChange={allSharedChatsPanel.onOpenChange}
searchSpaceId={allSharedChatsPanel.searchSpaceId}
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
/>
)}
{/* Mobile All Private Chats - slide-out panel */}
{allPrivateChatsPanel && (
<AllPrivateChatsSidebar
open={allPrivateChatsPanel.open}
onOpenChange={allPrivateChatsPanel.onOpenChange}
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
/>
)}
</div>
</TooltipProvider>
</SidebarProvider>

View file

@ -4,8 +4,10 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import {
ArchiveIcon,
ChevronLeft,
MessageCircleMore,
MoreHorizontal,
PenLine,
RotateCcwIcon,
Search,
Trash2,
@ -17,6 +19,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 +79,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 +201,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("");
}, []);
@ -205,6 +248,17 @@ export function AllPrivateChatsSidebar({
>
<div className="shrink-0 p-4 pb-2 space-y-3">
<div className="flex items-center gap-2">
{isMobile && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => onOpenChange(false)}
>
<ChevronLeft className="h-4 w-4 text-muted-foreground" />
<span className="sr-only">{t("close") || "Close"}</span>
</Button>
)}
<User className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
</div>
@ -356,6 +410,14 @@ export function AllPrivateChatsSidebar({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 z-80">
{!thread.archived && (
<DropdownMenuItem
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}
@ -412,6 +474,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>
);
}

View file

@ -4,8 +4,10 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import {
ArchiveIcon,
ChevronLeft,
MessageCircleMore,
MoreHorizontal,
PenLine,
RotateCcwIcon,
Search,
Trash2,
@ -17,6 +19,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 +79,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 +201,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("");
}, []);
@ -205,6 +248,17 @@ export function AllSharedChatsSidebar({
>
<div className="shrink-0 p-4 pb-2 space-y-3">
<div className="flex items-center gap-2">
{isMobile && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => onOpenChange(false)}
>
<ChevronLeft className="h-4 w-4 text-muted-foreground" />
<span className="sr-only">{t("close") || "Close"}</span>
</Button>
)}
<Users className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
</div>
@ -356,6 +410,14 @@ export function AllSharedChatsSidebar({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 z-80">
{!thread.archived && (
<DropdownMenuItem
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}
@ -412,6 +474,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>
);
}

View file

@ -4,7 +4,7 @@ import {
ArchiveIcon,
MessageSquare,
MoreHorizontal,
PencilIcon,
PenLine,
RotateCcwIcon,
Trash2,
} from "lucide-react";
@ -74,7 +74,7 @@ export function ChatListItem({
onRename();
}}
>
<PencilIcon className="mr-2 h-4 w-4" />
<PenLine className="mr-2 h-4 w-4" />
<span>{t("rename") || "Rename"}</span>
</DropdownMenuItem>
)}

View file

@ -727,7 +727,7 @@ export function InboxSidebar({
</Tooltip>
<DropdownMenuContent
align="end"
className={cn("z-80", activeTab === "status" ? "w-52" : "w-44")}
className={cn("z-80 select-none", activeTab === "status" ? "w-52" : "w-44")}
>
<DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal">
{t("filter") || "Filter"}

View file

@ -131,7 +131,7 @@ export function MobileSidebar({
</div>
{/* Sidebar Content - right side */}
<div className="flex-1 overflow-hidden flex flex-col">
<div className="flex-1 overflow-hidden flex flex-col [&>*]:!w-full">
<Sidebar
searchSpace={searchSpace}
isCollapsed={false}

View file

@ -93,7 +93,7 @@ export function Sidebar({
return (
<div
className={cn(
"relative flex h-full flex-col bg-sidebar text-sidebar-foreground overflow-hidden",
"relative flex h-full flex-col bg-sidebar text-sidebar-foreground overflow-hidden select-none",
isCollapsed ? "w-[60px] transition-all duration-200" : "",
!isCollapsed && !isResizing ? "transition-all duration-200" : "",
className

View file

@ -1,6 +1,6 @@
"use client";
import { ChevronsUpDown, Settings, Users } from "lucide-react";
import { ChevronsUpDown, Settings, UserPen } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
@ -51,14 +51,14 @@ export function SidebarHeader({
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem onClick={onManageMembers}>
<Users className="mr-2 h-4 w-4" />
<UserPen className="h-4 w-4" />
{t("manage_members")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onSettings}>
<Settings className="mr-2 h-4 w-4" />
<Settings className="h-4 w-4" />
{t("search_space_settings")}
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -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"

View file

@ -177,7 +177,7 @@ export function SidebarUserProfile({
<TooltipContent side="right">{displayName}</TooltipContent>
</Tooltip>
<DropdownMenuContent className="w-56" side="right" align="center" sideOffset={8}>
<DropdownMenuContent className="w-48" side="right" align="center" sideOffset={8}>
<DropdownMenuLabel className="font-normal">
<div className="flex items-center gap-2">
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
@ -191,14 +191,14 @@ export function SidebarUserProfile({
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onUserSettings}>
<Settings className="mr-2 h-4 w-4" />
<Settings className="h-4 w-4" />
{t("user_settings")}
</DropdownMenuItem>
{setTheme && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Sun className="mr-2 h-4 w-4" />
<Sun className="h-4 w-4" />
{t("theme")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
@ -216,7 +216,7 @@ export function SidebarUserProfile({
isSelected && "text-primary"
)}
>
<Icon className="mr-2 h-4 w-4" />
<Icon className="h-4 w-4" />
<span className="flex-1">{t(themeOption.value)}</span>
{isSelected && <Check className="h-4 w-4 shrink-0" />}
</DropdownMenuItem>
@ -229,7 +229,7 @@ export function SidebarUserProfile({
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Languages className="mr-2 h-4 w-4" />
<Languages className="h-4 w-4" />
{t("language")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
@ -262,7 +262,7 @@ export function SidebarUserProfile({
{isLoggingOut ? (
<Spinner size="sm" className="mr-2" />
) : (
<LogOut className="mr-2 h-4 w-4" />
<LogOut className="h-4 w-4" />
)}
{isLoggingOut ? t("loggingOut") : t("logout")}
</DropdownMenuItem>
@ -299,7 +299,7 @@ export function SidebarUserProfile({
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" side="top" align="center" sideOffset={4}>
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>
<DropdownMenuLabel className="font-normal">
<div className="flex items-center gap-2">
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
@ -313,14 +313,14 @@ export function SidebarUserProfile({
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onUserSettings}>
<Settings className="mr-2 h-4 w-4" />
<Settings className="h-4 w-4" />
{t("user_settings")}
</DropdownMenuItem>
{setTheme && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Sun className="mr-2 h-4 w-4" />
<Sun className="h-4 w-4" />
{t("theme")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
@ -338,7 +338,7 @@ export function SidebarUserProfile({
isSelected && "text-primary"
)}
>
<Icon className="mr-2 h-4 w-4" />
<Icon className="h-4 w-4" />
<span className="flex-1">{t(themeOption.value)}</span>
{isSelected && <Check className="h-4 w-4 shrink-0" />}
</DropdownMenuItem>
@ -351,7 +351,7 @@ export function SidebarUserProfile({
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Languages className="mr-2 h-4 w-4" />
<Languages className="h-4 w-4" />
{t("language")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
@ -381,11 +381,7 @@ export function SidebarUserProfile({
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
{isLoggingOut ? (
<Spinner size="sm" className="mr-2" />
) : (
<LogOut className="mr-2 h-4 w-4" />
)}
{isLoggingOut ? <Spinner size="sm" className="mr-2" /> : <LogOut className="h-4 w-4" />}
{isLoggingOut ? t("loggingOut") : t("logout")}
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -144,7 +144,9 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
<TooltipTrigger asChild>
<button
type="button"
onClick={() => router.push(`/dashboard/${params.search_space_id}/settings`)}
onClick={() =>
router.push(`/dashboard/${params.search_space_id}/settings?section=public-links`)
}
className="flex items-center justify-center h-8 w-8 rounded-md bg-muted/50 hover:bg-muted transition-colors"
>
<Earth className="h-4 w-4 text-muted-foreground" />
@ -181,7 +183,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
sideOffset={8}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="p-1.5 space-y-1">
<div className="p-1.5 space-y-1 select-none">
{/* Visibility Options */}
{visibilityOptions.map((option) => {
const isSelected = currentVisibility === option.value;

View file

@ -226,7 +226,7 @@ export function ModelSelector({
size="sm"
role="combobox"
aria-expanded={open}
className={cn("h-8 gap-2 px-3 text-sm border-border/60", className)}
className={cn("h-8 gap-2 px-3 text-sm border-border/60 select-none", className)}
>
{isLoading ? (
<>
@ -280,7 +280,7 @@ export function ModelSelector({
</PopoverTrigger>
<PopoverContent
className="w-[280px] md:w-[360px] p-0 rounded-lg shadow-lg border-border/60"
className="w-[280px] md:w-[360px] p-0 rounded-lg shadow-lg border-border/60 dark:bg-muted dark:border dark:border-neutral-700 select-none"
align="start"
sideOffset={8}
>
@ -289,18 +289,18 @@ export function ModelSelector({
onValueChange={(v) => setActiveTab(v as "llm" | "image")}
className="w-full"
>
<div className="border-b border-border/40">
<TabsList className="w-full grid grid-cols-2 rounded-none rounded-t-lg bg-card h-11 p-0 gap-0">
<div className="border-b border-border/80 dark:border-white/5">
<TabsList className="w-full grid grid-cols-2 rounded-none rounded-t-lg bg-transparent h-11 p-0 gap-0">
<TabsTrigger
value="llm"
className="relative gap-2 text-sm font-medium rounded-none text-muted-foreground/60 transition-all duration-200 h-full data-[state=active]:bg-transparent data-[state=active]:text-foreground data-[state=active]:shadow-none data-[state=active]:after:absolute data-[state=active]:after:bottom-0 data-[state=active]:after:left-3 data-[state=active]:after:right-3 data-[state=active]:after:h-[2px] data-[state=active]:after:bg-white data-[state=active]:after:rounded-full"
className="gap-2 text-sm font-medium rounded-none text-muted-foreground/60 transition-all duration-200 h-full bg-transparent data-[state=active]:bg-transparent shadow-none data-[state=active]:shadow-none border-b-[1.5px] border-transparent data-[state=active]:border-foreground dark:data-[state=active]:border-white data-[state=active]:text-foreground"
>
<Zap className="size-4" />
LLM
</TabsTrigger>
<TabsTrigger
value="image"
className="relative gap-2 text-sm font-medium rounded-none text-muted-foreground/60 transition-all duration-200 h-full data-[state=active]:bg-transparent data-[state=active]:text-foreground data-[state=active]:shadow-none data-[state=active]:after:absolute data-[state=active]:after:bottom-0 data-[state=active]:after:left-3 data-[state=active]:after:right-3 data-[state=active]:after:h-[2px] data-[state=active]:after:bg-white data-[state=active]:after:rounded-full"
className="gap-2 text-sm font-medium rounded-none text-muted-foreground/60 transition-all duration-200 h-full bg-transparent data-[state=active]:bg-transparent shadow-none data-[state=active]:shadow-none border-b-[1.5px] border-transparent data-[state=active]:border-foreground dark:data-[state=active]:border-white data-[state=active]:text-foreground"
>
<ImageIcon className="size-4" />
Image
@ -312,7 +312,7 @@ export function ModelSelector({
<TabsContent value="llm" className="mt-0">
<Command
shouldFilter={false}
className="rounded-none rounded-b-lg relative [&_[data-slot=command-input-wrapper]]:border-0 [&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:gap-2"
className="rounded-none rounded-b-lg relative dark:bg-muted [&_[data-slot=command-input-wrapper]]:border-0 [&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:gap-2"
>
{totalLLMModels > 3 && (
<div className="px-2 md:px-3 py-1.5 md:py-2">
@ -350,9 +350,9 @@ export function ModelSelector({
onSelect={() => handleSelectLLM(config)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
"hover:bg-accent/50",
isSelected && "bg-accent/80",
isAutoMode && "border border-violet-800"
"hover:bg-accent/50 dark:hover:bg-white/10",
isSelected && "bg-accent/80 dark:bg-white/10",
isAutoMode && ""
)}
>
<div className="flex items-center justify-between w-full gap-2">
@ -366,7 +366,7 @@ export function ModelSelector({
{isAutoMode && (
<Badge
variant="secondary"
className="text-[9px] px-1 py-0 h-3.5 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300 border-0"
className="text-[9px] px-1 py-0 h-3.5 bg-violet-800 text-white dark:bg-violet-800 dark:text-white border-0"
>
Recommended
</Badge>
@ -426,8 +426,8 @@ export function ModelSelector({
onSelect={() => handleSelectLLM(config)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
"hover:bg-accent/50",
isSelected && "bg-accent/80"
"hover:bg-accent/50 dark:hover:bg-white/10",
isSelected && "bg-accent/80 dark:bg-white/10"
)}
>
<div className="flex items-center justify-between w-full gap-2">
@ -471,11 +471,11 @@ export function ModelSelector({
)}
{/* Add New LLM Config */}
<div className="p-2 bg-muted/20">
<div className="p-2 bg-muted/20 dark:bg-muted">
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2 h-9 rounded-lg hover:bg-accent/50"
className="w-full justify-start gap-2 h-9 rounded-lg hover:bg-accent/50 dark:hover:bg-white/10"
onClick={() => {
setOpen(false);
onAddNewLLM();
@ -493,7 +493,7 @@ export function ModelSelector({
<TabsContent value="image" className="mt-0">
<Command
shouldFilter={false}
className="rounded-none rounded-b-lg [&_[data-slot=command-input-wrapper]]:border-0 [&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:gap-2"
className="rounded-none rounded-b-lg dark:bg-muted [&_[data-slot=command-input-wrapper]]:border-0 [&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:gap-2"
>
{totalImageModels > 3 && (
<div className="px-2 md:px-3 py-1.5 md:py-2">
@ -528,9 +528,9 @@ export function ModelSelector({
value={`img-g-${config.id}`}
onSelect={() => handleSelectImage(config.id)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80",
isAuto && "border border-violet-800"
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50 dark:hover:bg-white/10",
isSelected && "bg-accent/80 dark:bg-white/10",
isAuto && ""
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
@ -543,7 +543,7 @@ export function ModelSelector({
{isAuto && (
<Badge
variant="secondary"
className="text-[9px] px-1 py-0 h-3.5 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300 border-0"
className="text-[9px] px-1 py-0 h-3.5 bg-violet-800 text-white dark:bg-violet-800 dark:text-white border-0"
>
Recommended
</Badge>
@ -593,8 +593,8 @@ export function ModelSelector({
value={`img-u-${config.id}`}
onSelect={() => handleSelectImage(config.id)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80"
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50 dark:hover:bg-white/10",
isSelected && "bg-accent/80 dark:bg-white/10"
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
@ -634,11 +634,11 @@ export function ModelSelector({
{/* Add New Image Config */}
{onAddNewImage && (
<div className="p-2 bg-muted/20">
<div className="p-2 bg-muted/20 dark:bg-muted">
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2 h-9 rounded-lg hover:bg-accent/50"
className="w-full justify-start gap-2 h-9 rounded-lg hover:bg-accent/50 dark:hover:bg-white/10"
onClick={() => {
setOpen(false);
onAddNewImage();

View file

@ -35,12 +35,12 @@ export function PublicChatSnapshotRow({
memberMap,
}: PublicChatSnapshotRowProps) {
const [copied, setCopied] = useState(false);
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const handleCopyClick = useCallback(() => {
onCopy(snapshot);
setCopied(true);
clearTimeout(copyTimeoutRef.current);
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
}, [onCopy, snapshot]);
@ -117,12 +117,14 @@ export function PublicChatSnapshotRow({
{/* Public URL selectable fallback for manual copy */}
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<p
className="min-w-0 flex-1 text-[10px] font-mono text-muted-foreground break-all select-all cursor-text"
title={snapshot.public_url}
>
{snapshot.public_url}
</p>
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
<p
className="text-[10px] font-mono text-muted-foreground whitespace-nowrap select-all cursor-text"
title={snapshot.public_url}
>
{snapshot.public_url}
</p>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>

View file

@ -334,7 +334,7 @@ function ReportPanelContent({
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={`min-w-[180px] dark:bg-neutral-800 dark:border dark:border-neutral-700${insideDrawer ? " z-[100]" : ""}`}
className={`min-w-[180px] bg-muted dark:border dark:border-neutral-700${insideDrawer ? " z-[100]" : ""}`}
>
<DropdownMenuItem onClick={() => handleExport("md")}>
Download Markdown
@ -361,56 +361,30 @@ function ReportPanelContent({
</DropdownMenu>
{/* Version switcher — only shown when multiple versions exist */}
{versions.length > 1 &&
(insideDrawer ? (
/* Mobile: compact dropdown */
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 px-3.5 py-4 text-[15px] gap-1.5"
{versions.length > 1 && (
<DropdownMenu modal={insideDrawer ? false : undefined}>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 px-3.5 py-4 text-[15px] gap-1.5">
v{activeVersionIndex + 1}
<ChevronDownIcon className="size-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={`min-w-[120px] bg-muted dark:border dark:border-neutral-700${insideDrawer ? " z-[100]" : ""}`}
>
{versions.map((v, i) => (
<DropdownMenuItem
key={v.id}
onClick={() => setActiveReportId(v.id)}
className={v.id === activeReportId ? "bg-accent font-medium" : ""}
>
v{activeVersionIndex + 1}
<ChevronDownIcon className="size-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-[120px] z-[100]">
{versions.map((v, i) => (
<DropdownMenuItem
key={v.id}
onClick={() => setActiveReportId(v.id)}
className={v.id === activeReportId ? "bg-accent font-medium" : ""}
>
Version {i + 1}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : (
/* Desktop: inline version buttons */
<div className="flex items-center gap-1">
<div className="flex items-center gap-0.5 rounded-lg border bg-muted/30 p-0.5">
{versions.map((v, i) => (
<button
key={v.id}
type="button"
onClick={() => setActiveReportId(v.id)}
className={`px-2 py-0.5 rounded-md text-xs font-medium transition-colors ${
v.id === activeReportId
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
>
v{i + 1}
</button>
))}
</div>
<span className="text-[10px] text-muted-foreground tabular-nums ml-1">
{activeVersionIndex + 1} of {versions.length}
</span>
</div>
))}
Version {i + 1}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
{onClose && (
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">

File diff suppressed because it is too large Load diff

View file

@ -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")}