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

@ -373,10 +373,11 @@ async def read_documents(
# Convert database objects to API-friendly format
api_documents = []
for doc in db_documents:
# Get user name (display_name or email fallback)
created_by_name = None
created_by_email = None
if doc.created_by:
created_by_name = doc.created_by.display_name or doc.created_by.email
created_by_name = doc.created_by.display_name
created_by_email = doc.created_by.email
# Parse status from JSONB
status_data = None
@ -400,6 +401,7 @@ async def read_documents(
search_space_id=doc.search_space_id,
created_by_id=doc.created_by_id,
created_by_name=created_by_name,
created_by_email=created_by_email,
status=status_data,
)
)
@ -528,10 +530,11 @@ async def search_documents(
# Convert database objects to API-friendly format
api_documents = []
for doc in db_documents:
# Get user name (display_name or email fallback)
created_by_name = None
created_by_email = None
if doc.created_by:
created_by_name = doc.created_by.display_name or doc.created_by.email
created_by_name = doc.created_by.display_name
created_by_email = doc.created_by.email
# Parse status from JSONB
status_data = None
@ -555,6 +558,7 @@ async def search_documents(
search_space_id=doc.search_space_id,
created_by_id=doc.created_by_id,
created_by_name=created_by_name,
created_by_email=created_by_email,
status=status_data,
)
)

View file

@ -60,9 +60,8 @@ class DocumentRead(BaseModel):
updated_at: datetime | None
search_space_id: int
created_by_id: UUID | None = None # User who created/uploaded this document
created_by_name: str | None = (
None # Display name or email of the user who created this document
)
created_by_name: str | None = None
created_by_email: str | None = None
status: DocumentStatusSchema | None = (
None # Processing status (ready, processing, failed)
)

View file

@ -67,7 +67,7 @@ export function DocumentTypeChip({ type, className }: { type: string; className?
const chip = (
<span
className={`inline-flex items-center gap-1.5 rounded-full bg-accent/80 px-2.5 py-1 text-xs font-medium text-accent-foreground/70 shadow-sm max-w-full overflow-hidden ${className ?? ""}`}
className={`inline-flex items-center gap-1.5 rounded-full bg-accent/80 px-2.5 py-1 text-xs font-medium text-accent-foreground shadow-sm max-w-full overflow-hidden ${className ?? ""}`}
>
<span className="flex-shrink-0">{icon}</span>
<span ref={textRef} className="truncate min-w-0">

View file

@ -3,13 +3,13 @@
import { useSetAtom } from "jotai";
import {
CircleAlert,
CircleX,
FilePlus2,
FileType,
ListFilter,
Search,
SlidersHorizontal,
Trash,
Upload,
X,
} from "lucide-react";
import { motion } from "motion/react";
import { useTranslations } from "next-intl";
@ -81,7 +81,7 @@ export function DocumentsFilters({
return (
<motion.div
className="flex flex-col gap-4"
className="flex flex-col gap-4 select-none"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.1 }}
@ -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
@ -126,7 +126,7 @@ export function DocumentsFilters({
<Input
id={`${id}-input`}
ref={inputRef}
className="peer h-9 w-full pl-9 pr-9 text-sm bg-background border-border/60 focus-visible:ring-1 focus-visible:ring-ring/30"
className="peer h-9 w-full pl-9 pr-9 text-sm bg-background border-border/60 focus-visible:ring-1 focus-visible:ring-ring/30 select-none focus:select-text"
value={searchValue}
onChange={(e) => onSearch(e.target.value)}
placeholder="Filter by title"
@ -135,7 +135,7 @@ export function DocumentsFilters({
/>
{Boolean(searchValue) && (
<motion.button
className="absolute inset-y-0 right-0 flex h-full w-9 items-center justify-center rounded-r-md text-muted-foreground/60 hover:text-foreground transition-colors"
className="absolute inset-y-0 right-0 flex h-full w-9 items-center justify-center rounded-r-md text-muted-foreground hover:text-foreground transition-colors"
aria-label="Clear filter"
onClick={() => {
onSearch("");
@ -147,7 +147,7 @@ export function DocumentsFilters({
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<CircleX size={14} strokeWidth={2} aria-hidden="true" />
<X size={14} strokeWidth={2} aria-hidden="true" />
</motion.button>
)}
</motion.div>

View file

@ -336,7 +336,7 @@ export function DocumentsTableShell({
return (
<motion.div
className="rounded-lg border border-border/40 bg-background overflow-hidden"
className="rounded-lg border border-border/40 bg-background overflow-hidden select-none"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.2 }}
@ -453,7 +453,7 @@ export function DocumentsTableShell({
) : error ? (
<div className="flex h-[50vh] w-full items-center justify-center">
<div className="flex flex-col items-center gap-3">
<AlertCircle className="h-8 w-8 text-destructive/60" />
<AlertCircle className="h-8 w-8 text-destructive" />
<p className="text-sm text-destructive">{t("error_loading")}</p>
</div>
</div>
@ -482,7 +482,7 @@ export function DocumentsTableShell({
</div>
) : (
<>
{/* Desktop Table View - Notion Style */}
{/* Desktop Table View */}
<div className="hidden md:flex md:flex-col">
{/* Fixed Header */}
<Table className="table-fixed w-full">
@ -629,7 +629,24 @@ export function DocumentsTableShell({
)}
{columnVisibility.created_by && (
<TableCell className="w-36 py-2.5 text-sm text-foreground truncate border-r border-border/40">
{doc.created_by_name || "—"}
{doc.created_by_name ? (
doc.created_by_email ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default truncate block">
{doc.created_by_name}
</span>
</TooltipTrigger>
<TooltipContent side="top" align="start">
{doc.created_by_email}
</TooltipContent>
</Tooltip>
) : (
<span className="truncate block">{doc.created_by_name}</span>
)
) : (
<span className="truncate block">{doc.created_by_email || "—"}</span>
)}
</TableCell>
)}
{columnVisibility.created_at && (
@ -765,11 +782,11 @@ export function DocumentsTableShell({
{/* Document Content Viewer - lazy loads content on-demand */}
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col overflow-hidden pb-0">
<DialogHeader className="flex-shrink-0">
<DialogTitle>{viewingDoc?.title}</DialogTitle>
</DialogHeader>
<div className="mt-4">
<div className="mt-4 overflow-y-auto flex-1 min-h-0 px-6 select-text">
{viewingLoading ? (
<div className="flex items-center justify-center py-12">
<Spinner size="lg" className="text-muted-foreground" />

View file

@ -30,7 +30,7 @@ export function PaginationControls({
return (
<motion.div
className="flex items-center justify-end gap-3 py-3 px-2"
className="flex items-center justify-end gap-3 py-3 px-2 select-none"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.3 }}

View file

@ -16,6 +16,7 @@ export type Document = {
search_space_id: number;
created_by_id?: string | null;
created_by_name?: string | null;
created_by_email?: string | null;
status?: DocumentStatus;
};

View file

@ -115,6 +115,7 @@ export default function DocumentsTable() {
title: item.title,
created_by_id: item.created_by_id ?? null,
created_by_name: item.created_by_name ?? null,
created_by_email: item.created_by_email ?? null,
created_at: item.created_at,
status: (
item as {

View file

@ -29,7 +29,6 @@ import {
ChevronRight,
ChevronUp,
CircleAlert,
CircleX,
Clock,
Columns3,
Filter,
@ -741,7 +740,7 @@ function LogsFilters({
inputRef.current?.focus();
}}
>
<CircleX size={16} strokeWidth={2} />
<X size={16} strokeWidth={2} />
</Button>
)}
</motion.div>

View file

@ -75,7 +75,7 @@ export default function MorePagesPage() {
const allCompleted = data?.tasks.every((t) => t.completed) ?? false;
return (
<div className="flex min-h-[calc(100vh-64px)] items-center justify-center px-4 py-8">
<div className="flex min-h-[calc(100vh-64px)] select-none items-center justify-center px-4 py-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -174,7 +174,7 @@ export default function MorePagesPage() {
Contact Us
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogContent className="select-none sm:max-w-md">
<DialogHeader>
<DialogTitle>Contact Us</DialogTitle>
<DialogDescription>Schedule a meeting or send us an email.</DialogDescription>

View file

@ -259,7 +259,7 @@ export default function OnboardPage() {
You can add more configurations and customize settings anytime in{" "}
<button
type="button"
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings`)}
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings?section=general`)}
className="text-violet-500 hover:underline"
>
Settings

View file

@ -12,10 +12,11 @@ import {
Menu,
MessageSquare,
Settings,
Shield,
X,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { PublicChatSnapshotsManager } from "@/components/public-chat-snapshots/public-chat-snapshots-manager";
@ -24,6 +25,7 @@ import { ImageModelManager } from "@/components/settings/image-model-manager";
import { LLMRoleManager } from "@/components/settings/llm-role-manager";
import { ModelConfigManager } from "@/components/settings/model-config-manager";
import { PromptConfigManager } from "@/components/settings/prompt-config-manager";
import { RolesManager } from "@/components/settings/roles-manager";
import { Button } from "@/components/ui/button";
import { trackSettingsViewed } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
@ -72,6 +74,12 @@ const settingsNavItems: SettingsNavItem[] = [
descriptionKey: "nav_public_links_desc",
icon: Globe,
},
{
id: "team-roles",
labelKey: "nav_team_roles",
descriptionKey: "nav_team_roles_desc",
icon: Shield,
},
];
function SettingsSidebar({
@ -240,7 +248,7 @@ function SettingsContent({
{/* Section Header */}
<AnimatePresence mode="wait">
<motion.div
key={activeSection + "-header"}
key={`${activeSection}-header`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
@ -298,6 +306,7 @@ function SettingsContent({
{activeSection === "public-links" && (
<PublicChatSnapshotsManager searchSpaceId={searchSpaceId} />
)}
{activeSection === "team-roles" && <RolesManager searchSpaceId={searchSpaceId} />}
</motion.div>
</AnimatePresence>
</div>
@ -306,14 +315,27 @@ function SettingsContent({
);
}
const VALID_SECTIONS = new Set(settingsNavItems.map((item) => item.id));
const DEFAULT_SECTION = "general";
export default function SettingsPage() {
const router = useRouter();
const params = useParams();
const searchParams = useSearchParams();
const searchSpaceId = Number(params.search_space_id);
const [activeSection, setActiveSection] = useState("general");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
// Track settings section view
const sectionParam = searchParams.get("section");
const activeSection =
sectionParam && VALID_SECTIONS.has(sectionParam) ? sectionParam : DEFAULT_SECTION;
const handleSectionChange = useCallback(
(section: string) => {
router.replace(`/dashboard/${searchSpaceId}/settings?section=${section}`, { scroll: false });
},
[router, searchSpaceId]
);
useEffect(() => {
trackSettingsViewed(searchSpaceId, activeSection);
}, [searchSpaceId, activeSection]);
@ -333,7 +355,7 @@ export default function SettingsPage() {
<div className="flex h-full w-full overflow-hidden bg-background md:rounded-xl md:border md:shadow-sm">
<SettingsSidebar
activeSection={activeSection}
onSectionChange={setActiveSection}
onSectionChange={handleSectionChange}
onBackToApp={handleBackToApp}
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}

File diff suppressed because it is too large Load diff

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

View file

@ -44,6 +44,7 @@ export const document = z.object({
search_space_id: z.number(),
created_by_id: z.string().nullable().optional(),
created_by_name: z.string().nullable().optional(),
created_by_email: z.string().nullable().optional(),
});
export const extensionDocumentContent = z.object({

View file

@ -26,7 +26,7 @@ interface DocumentElectric {
status: DocumentStatusType | null;
}
// Document for display (with resolved user name)
// Document for display (with resolved user name and email)
export interface DocumentDisplay {
id: number;
search_space_id: number;
@ -34,6 +34,7 @@ export interface DocumentDisplay {
title: string;
created_by_id: string | null;
created_by_name: string | null;
created_by_email: string | null;
created_at: string;
status: DocumentStatusType;
}
@ -94,8 +95,9 @@ export function useDocuments(
// Track if initial API load is complete (source of truth)
const apiLoadedRef = useRef(false);
// User cache: userId → displayName
// User cache: userId → displayName / email
const userCacheRef = useRef<Map<string, string>>(new Map());
const emailCacheRef = useRef<Map<string, string>>(new Map());
// Electric sync refs
const syncHandleRef = useRef<SyncHandle | null>(null);
@ -119,10 +121,21 @@ export function useDocuments(
// Populate user cache from API response
const populateUserCache = useCallback(
(items: Array<{ created_by_id?: string | null; created_by_name?: string | null }>) => {
(
items: Array<{
created_by_id?: string | null;
created_by_name?: string | null;
created_by_email?: string | null;
}>
) => {
for (const item of items) {
if (item.created_by_id && item.created_by_name) {
userCacheRef.current.set(item.created_by_id, item.created_by_name);
if (item.created_by_id) {
if (item.created_by_name) {
userCacheRef.current.set(item.created_by_id, item.created_by_name);
}
if (item.created_by_email) {
emailCacheRef.current.set(item.created_by_id, item.created_by_email);
}
}
}
},
@ -138,6 +151,7 @@ export function useDocuments(
title: string;
created_by_id?: string | null;
created_by_name?: string | null;
created_by_email?: string | null;
created_at: string;
status?: DocumentStatusType | null;
}): DocumentDisplay => ({
@ -147,6 +161,7 @@ export function useDocuments(
title: item.title,
created_by_id: item.created_by_id ?? null,
created_by_name: item.created_by_name ?? null,
created_by_email: item.created_by_email ?? null,
created_at: item.created_at,
status: item.status ?? { state: "ready" },
}),
@ -160,6 +175,9 @@ export function useDocuments(
created_by_name: doc.created_by_id
? (userCacheRef.current.get(doc.created_by_id) ?? null)
: null,
created_by_email: doc.created_by_id
? (emailCacheRef.current.get(doc.created_by_id) ?? null)
: null,
status: doc.status ?? { state: "ready" },
}),
[]
@ -351,6 +369,9 @@ export function useDocuments(
created_by_name: doc.created_by_id
? (userCacheRef.current.get(doc.created_by_id) ?? null)
: null,
created_by_email: doc.created_by_id
? (emailCacheRef.current.get(doc.created_by_id) ?? null)
: null,
}))
);
}
@ -455,6 +476,7 @@ export function useDocuments(
setAllDocuments([]);
apiLoadedRef.current = false;
userCacheRef.current.clear();
emailCacheRef.current.clear();
}
prevSearchSpaceIdRef.current = searchSpaceId;
}, [searchSpaceId]);

View file

@ -43,7 +43,7 @@ export function getProviderIcon(
{ isAutoMode, className = "size-4" }: { isAutoMode?: boolean; className?: string } = {}
) {
if (isAutoMode || provider?.toUpperCase() === "AUTO") {
return <Shuffle className={cn(className, "text-violet-800")} />;
return <Shuffle className={cn(className, "text-muted-foreground")} />;
}
switch (provider?.toUpperCase()) {

View file

@ -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",
@ -746,6 +747,8 @@
"nav_system_instructions_desc": "SearchSpace-wide AI instructions",
"nav_public_links": "Public Chat Links",
"nav_public_links_desc": "Manage publicly shared chat links",
"nav_team_roles": "Team Roles",
"nav_team_roles_desc": "Manage team roles & permissions",
"general_name_label": "Name",
"general_name_placeholder": "Enter search space name",
"general_name_description": "A unique name for your search space.",

View file

@ -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",
@ -746,6 +747,8 @@
"nav_system_instructions_desc": "Instrucciones de IA a nivel del espacio de búsqueda",
"nav_public_links": "Enlaces de chat públicos",
"nav_public_links_desc": "Administrar enlaces de chat compartidos públicamente",
"nav_team_roles": "Team Roles",
"nav_team_roles_desc": "Manage team roles & permissions",
"general_name_label": "Nombre",
"general_name_placeholder": "Ingresa el nombre del espacio de búsqueda",
"general_name_description": "Un nombre único para tu espacio de búsqueda.",

View file

@ -720,7 +720,8 @@
"unread": "अपठित",
"connectors": "कनेक्टर",
"all_connectors": "सभी कनेक्टर",
"close": "बंद करें"
"close": "बंद करें",
"cancel": "रद्द करें"
},
"errors": {
"something_went_wrong": "कुछ गलत हो गया",
@ -746,6 +747,8 @@
"nav_system_instructions_desc": "सर्च स्पेस-व्यापी AI निर्देश",
"nav_public_links": "सार्वजनिक चैट लिंक",
"nav_public_links_desc": "सार्वजनिक रूप से साझा किए गए चैट लिंक प्रबंधित करें",
"nav_team_roles": "Team Roles",
"nav_team_roles_desc": "Manage team roles & permissions",
"general_name_label": "नाम",
"general_name_placeholder": "सर्च स्पेस का नाम दर्ज करें",
"general_name_description": "आपके सर्च स्पेस के लिए एक अद्वितीय नाम।",

View file

@ -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",
@ -746,6 +747,8 @@
"nav_system_instructions_desc": "Instruções de IA em nível do espaço de pesquisa",
"nav_public_links": "Links de chat públicos",
"nav_public_links_desc": "Gerenciar links de chat compartilhados publicamente",
"nav_team_roles": "Team Roles",
"nav_team_roles_desc": "Manage team roles & permissions",
"general_name_label": "Nome",
"general_name_placeholder": "Insira o nome do espaço de pesquisa",
"general_name_description": "Um nome único para seu espaço de pesquisa.",

View file

@ -704,7 +704,8 @@
"unread": "未读",
"connectors": "连接器",
"all_connectors": "所有连接器",
"close": "关闭"
"close": "关闭",
"cancel": "取消"
},
"errors": {
"something_went_wrong": "出错了",
@ -730,6 +731,8 @@
"nav_system_instructions_desc": "搜索空间级别的 AI 指令",
"nav_public_links": "公开聊天链接",
"nav_public_links_desc": "管理公开分享的聊天链接",
"nav_team_roles": "Team Roles",
"nav_team_roles_desc": "Manage team roles & permissions",
"general_name_label": "名称",
"general_name_placeholder": "输入搜索空间名称",
"general_name_description": "您的搜索空间的唯一名称。",