mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
feat: implement responsive row action dropdowns and enhance mobile sidebar navigation
This commit is contained in:
parent
a10bfe32cd
commit
3bea989868
16 changed files with 256 additions and 191 deletions
|
|
@ -257,8 +257,10 @@ export function DashboardClientLayout({
|
||||||
<div className="flex items-center justify-between w-full gap-2 px-4">
|
<div className="flex items-center justify-between w-full gap-2 px-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="h-6" />
|
<div className="hidden md:flex items-center gap-2">
|
||||||
<DashboardBreadcrumb />
|
<Separator orientation="vertical" className="h-6" />
|
||||||
|
<DashboardBreadcrumb />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
|
||||||
|
|
@ -278,14 +278,17 @@ export default function ConnectorsPage() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="mb-8 flex items-center justify-between"
|
className="mb-8 flex items-center justify-between gap-2"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
|
<h1 className="text-xl md:text-3xl font-bold tracking-tight">{t("title")}</h1>
|
||||||
<p className="text-muted-foreground mt-2">{t("subtitle")}</p>
|
<p className="text-xs md:text-base text-muted-foreground mt-2">{t("subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}>
|
<Button
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
className="h-8 text-xs px-3 md:h-10 md:text-sm md:px-4"
|
||||||
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-3 w-3 md:h-4 md:w-4" />
|
||||||
{t("add_connector")}
|
{t("add_connector")}
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,14 @@ export function DocumentsFilters({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex flex-wrap items-center justify-between gap-3"
|
className="flex flex-wrap items-center justify-start gap-3 w-full"
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.1 }}
|
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative"
|
className="relative w-full sm:w-auto"
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||||
|
|
@ -90,7 +90,7 @@ export function DocumentsFilters({
|
||||||
<Input
|
<Input
|
||||||
id={`${id}-input`}
|
id={`${id}-input`}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="peer min-w-60 ps-9"
|
className="peer w-full sm:min-w-60 ps-9"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => onSearch(e.target.value)}
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
placeholder={t("filter_placeholder")}
|
placeholder={t("filter_placeholder")}
|
||||||
|
|
@ -231,11 +231,11 @@ export function DocumentsFilters({
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
|
||||||
{selectedIds.size > 0 && (
|
{selectedIds.size > 0 && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button className="ml-auto" variant="outline">
|
<Button className="w-full sm:w-auto" variant="outline">
|
||||||
<Trash
|
<Trash
|
||||||
className="-ms-1 me-2 opacity-60"
|
className="-ms-1 me-2 opacity-60"
|
||||||
size={16}
|
size={16}
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ export function DocumentsTableShell({
|
||||||
|
|
||||||
const toggleAll = (checked: boolean) => {
|
const toggleAll = (checked: boolean) => {
|
||||||
const next = new Set(selectedIds);
|
const next = new Set(selectedIds);
|
||||||
if (checked) sorted.forEach((d) => next.add(d.id));
|
if (checked) sorted.forEach((d) => { next.add(d.id); });
|
||||||
else sorted.forEach((d) => next.delete(d.id));
|
else sorted.forEach((d) => { next.delete(d.id); });
|
||||||
setSelectedIds(next);
|
setSelectedIds(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -323,26 +323,16 @@ export function DocumentsTableShell({
|
||||||
const icon = getDocumentTypeIcon(doc.document_type);
|
const icon = getDocumentTypeIcon(doc.document_type);
|
||||||
return (
|
return (
|
||||||
<div key={doc.id} className="p-3">
|
<div key={doc.id} className="p-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedIds.has(doc.id)}
|
checked={selectedIds.has(doc.id)}
|
||||||
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
|
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
|
||||||
aria-label="Select row"
|
aria-label="Select row"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<span className="text-muted-foreground shrink-0">{icon}</span>
|
||||||
<span className="text-muted-foreground shrink-0">{icon}</span>
|
<div className="font-medium truncate">{doc.title}</div>
|
||||||
<div className="font-medium truncate">{doc.title}</div>
|
|
||||||
</div>
|
|
||||||
<RowActions
|
|
||||||
document={doc}
|
|
||||||
deleteDocument={deleteDocument}
|
|
||||||
refreshDocuments={async () => {
|
|
||||||
await onRefresh();
|
|
||||||
}}
|
|
||||||
searchSpaceId={searchSpaceId as string}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
<DocumentTypeChip type={doc.document_type} />
|
<DocumentTypeChip type={doc.document_type} />
|
||||||
|
|
@ -371,6 +361,14 @@ export function DocumentsTableShell({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<RowActions
|
||||||
|
document={doc}
|
||||||
|
deleteDocument={deleteDocument}
|
||||||
|
refreshDocuments={async () => {
|
||||||
|
await onRefresh();
|
||||||
|
}}
|
||||||
|
searchSpaceId={searchSpaceId as string}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FileText, Pencil, Trash2 } from "lucide-react";
|
import { FileText, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
@ -16,6 +16,12 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import type { Document } from "./types";
|
import type { Document } from "./types";
|
||||||
|
|
||||||
|
|
@ -57,53 +63,108 @@ export function RowActions({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
{/* Edit Button */}
|
{/* Desktop Actions */}
|
||||||
<Tooltip>
|
<div className="hidden md:flex items-center gap-1">
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<motion.div
|
<TooltipTrigger asChild>
|
||||||
whileHover={{ scale: 1.1 }}
|
<motion.div
|
||||||
whileTap={{ scale: 0.95 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
|
|
||||||
onClick={handleEdit}
|
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Button
|
||||||
<span className="sr-only">Edit Document</span>
|
variant="ghost"
|
||||||
</Button>
|
size="icon"
|
||||||
</motion.div>
|
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
|
||||||
</TooltipTrigger>
|
onClick={handleEdit}
|
||||||
<TooltipContent side="top">
|
>
|
||||||
<p>Edit Document</p>
|
<Pencil className="h-4 w-4" />
|
||||||
</TooltipContent>
|
<span className="sr-only">Edit Document</span>
|
||||||
</Tooltip>
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
<p>Edit Document</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/* View Metadata Button */}
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<motion.div
|
||||||
<motion.div
|
whileHover={{ scale: 1.1 }}
|
||||||
whileHover={{ scale: 1.1 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
|
|
||||||
onClick={() => setIsMetadataOpen(true)}
|
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4" />
|
<Button
|
||||||
<span className="sr-only">View Metadata</span>
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
|
||||||
|
onClick={() => setIsMetadataOpen(true)}
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
<span className="sr-only">View Metadata</span>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
<p>View Metadata</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={() => setIsDeleteOpen(true)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Delete</span>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
<p>Delete</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Actions Dropdown */}
|
||||||
|
<div className="flex md:hidden">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</DropdownMenuTrigger>
|
||||||
</TooltipTrigger>
|
<DropdownMenuContent align="end" className="w-40">
|
||||||
<TooltipContent side="top">
|
<DropdownMenuItem onClick={handleEdit}>
|
||||||
<p>View Metadata</p>
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
</TooltipContent>
|
<span>Edit</span>
|
||||||
</Tooltip>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setIsMetadataOpen(true)}>
|
||||||
|
<FileText className="mr-2 h-4 w-4" />
|
||||||
|
<span>Metadata</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => setIsDeleteOpen(true)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
<JsonMetadataViewer
|
<JsonMetadataViewer
|
||||||
title={document.title}
|
title={document.title}
|
||||||
metadata={document.document_metadata}
|
metadata={document.document_metadata}
|
||||||
|
|
@ -111,30 +172,6 @@ export function RowActions({
|
||||||
onOpenChange={setIsMetadataOpen}
|
onOpenChange={setIsMetadataOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Button */}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
|
||||||
onClick={() => setIsDeleteOpen(true)}
|
|
||||||
disabled={isDeleting}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Delete</span>
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top">
|
|
||||||
<p>Delete</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|
|
||||||
|
|
@ -225,8 +225,8 @@ export default function DocumentsTable() {
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
|
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||||
<p className="text-muted-foreground">{t("subtitle")}</p>
|
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={refreshCurrentView} variant="outline" size="sm">
|
<Button onClick={refreshCurrentView} variant="outline" size="sm">
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
|
|
||||||
|
|
@ -506,8 +506,8 @@ export default function LogsManagePage() {
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
|
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||||
<p className="text-muted-foreground">{t("subtitle")}</p>
|
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleRefresh} variant="outline" size="sm">
|
<Button onClick={handleRefresh} variant="outline" size="sm">
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
|
@ -521,48 +521,10 @@ export default function LogsManagePage() {
|
||||||
uniqueLevels={uniqueLevels}
|
uniqueLevels={uniqueLevels}
|
||||||
uniqueStatuses={uniqueStatuses}
|
uniqueStatuses={uniqueStatuses}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
onBulkDelete={handleDeleteRows}
|
||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Button */}
|
|
||||||
{table.getSelectedRowModel().rows.length > 0 && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -10 }}
|
|
||||||
className="flex justify-end"
|
|
||||||
>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button variant="outline">
|
|
||||||
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
|
|
||||||
{t("delete_selected")}
|
|
||||||
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
|
|
||||||
{table.getSelectedRowModel().rows.length}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
|
|
||||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
|
|
||||||
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
</div>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleDeleteRows}>{t("delete")}</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Logs Table */}
|
{/* Logs Table */}
|
||||||
<LogsTable
|
<LogsTable
|
||||||
table={table}
|
table={table}
|
||||||
|
|
@ -713,29 +675,31 @@ function LogsFilters({
|
||||||
uniqueLevels,
|
uniqueLevels,
|
||||||
uniqueStatuses,
|
uniqueStatuses,
|
||||||
inputRef,
|
inputRef,
|
||||||
|
onBulkDelete,
|
||||||
id,
|
id,
|
||||||
}: {
|
}: {
|
||||||
table: any;
|
table: any;
|
||||||
uniqueLevels: string[];
|
uniqueLevels: string[];
|
||||||
uniqueStatuses: string[];
|
uniqueStatuses: string[];
|
||||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
onBulkDelete: () => Promise<void>;
|
||||||
id: string;
|
id: string;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("logs");
|
const t = useTranslations("logs");
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex flex-wrap items-center justify-between gap-3"
|
className="flex flex-wrap items-center justify-start gap-3 w-full"
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
|
||||||
{/* Search Input */}
|
{/* Search Input */}
|
||||||
<motion.div className="relative" variants={fadeInScale}>
|
<motion.div className="relative w-full sm:w-auto" variants={fadeInScale}>
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer min-w-60 ps-9",
|
"peer w-full sm:min-w-60 ps-9",
|
||||||
Boolean(table.getColumn("message")?.getFilterValue()) && "pe-9"
|
Boolean(table.getColumn("message")?.getFilterValue()) && "pe-9"
|
||||||
)}
|
)}
|
||||||
value={(table.getColumn("message")?.getFilterValue() ?? "") as string}
|
value={(table.getColumn("message")?.getFilterValue() ?? "") as string}
|
||||||
|
|
@ -806,6 +770,39 @@ function LogsFilters({
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
|
||||||
|
{table.getSelectedRowModel().rows.length > 0 && (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button className="w-full sm:w-auto" variant="outline">
|
||||||
|
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
|
||||||
|
{t("delete_selected")}
|
||||||
|
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
|
||||||
|
{table.getSelectedRowModel().rows.length}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
|
||||||
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
|
||||||
|
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
</div>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={onBulkDelete}>{t("delete")}</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -973,6 +970,7 @@ function LogsTable({
|
||||||
style={{ width: `${header.getSize()}px` }}
|
style={{ width: `${header.getSize()}px` }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-12 px-4 py-3",
|
"h-12 px-4 py-3",
|
||||||
|
header.column.id === "select" ? "ps-4 pe-0" : "",
|
||||||
// keep Created At header from wrapping and align it
|
// keep Created At header from wrapping and align it
|
||||||
header.column.id === "created_at" ? "whitespace-nowrap text-right" : ""
|
header.column.id === "created_at" ? "whitespace-nowrap text-right" : ""
|
||||||
)}
|
)}
|
||||||
|
|
@ -1030,7 +1028,8 @@ function LogsTable({
|
||||||
<TableCell
|
<TableCell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-4 py-3 align-middle overflow-hidden",
|
"px-4 py-3 align-middle",
|
||||||
|
cell.column.id === "select" ? "ps-4 pe-0" : "overflow-hidden",
|
||||||
isCreatedAt
|
isCreatedAt
|
||||||
? "whitespace-nowrap text-xs text-muted-foreground text-right"
|
? "whitespace-nowrap text-xs text-muted-foreground text-right"
|
||||||
: "",
|
: "",
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ export function LanguageSwitcher() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={locale} onValueChange={handleLanguageChange}>
|
<Select value={locale} onValueChange={handleLanguageChange}>
|
||||||
<SelectTrigger className="w-[160px]">
|
<SelectTrigger className="w-[110px] sm:w-[160px] h-8 sm:h-10 text-xs sm:text-sm px-2 sm:px-3 gap-1 sm:gap-2">
|
||||||
<Globe className="mr-2 h-4 w-4" />
|
<Globe className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{languages.find((lang) => lang.code === locale)?.name || "English"}
|
{languages.find((lang) => lang.code === locale)?.name || "English"}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,8 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea
|
||||||
"text-muted-foreground hover:text-foreground"
|
"text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header text with shimmer if processing or has in-progress step */}
|
{/* Header text with shimmer if processing (streaming) */}
|
||||||
{isProcessing && inProgressStep ? (
|
{isProcessing ? (
|
||||||
<TextShimmerLoader text={getHeaderText()} size="sm" />
|
<TextShimmerLoader text={getHeaderText()} size="sm" />
|
||||||
) : (
|
) : (
|
||||||
<span>{getHeaderText()}</span>
|
<span>{getHeaderText()}</span>
|
||||||
|
|
@ -398,7 +398,7 @@ const ThreadWelcome: FC = () => {
|
||||||
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
|
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
|
||||||
{/* Greeting positioned above the composer - fixed position */}
|
{/* Greeting positioned above the composer - fixed position */}
|
||||||
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
|
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
|
||||||
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-5xl delay-100 duration-500 ease-out fill-mode-both">
|
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-3xl md:text-5xl delay-100 duration-500 ease-out fill-mode-both">
|
||||||
{greeting}
|
{greeting}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -891,14 +891,17 @@ const ThinkingStepsPart: FC = () => {
|
||||||
const messageId = useAssistantState(({ message }) => message?.id);
|
const messageId = useAssistantState(({ message }) => message?.id);
|
||||||
const thinkingSteps = thinkingStepsMap.get(messageId) || [];
|
const thinkingSteps = thinkingStepsMap.get(messageId) || [];
|
||||||
|
|
||||||
// Check if thread is still running (for stopping the spinner when cancelled)
|
// Check if this specific message is currently streaming
|
||||||
|
// A message is streaming if: thread is running AND this is the last assistant message
|
||||||
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
||||||
|
const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false);
|
||||||
|
const isMessageStreaming = isThreadRunning && isLastMessage;
|
||||||
|
|
||||||
if (thinkingSteps.length === 0) return null;
|
if (thinkingSteps.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isThreadRunning} />
|
<ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isMessageStreaming} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,11 @@ export function JsonMetadataViewer({
|
||||||
if (open !== undefined && onOpenChange !== undefined) {
|
if (open !== undefined && onOpenChange !== undefined) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-4xl max-w-[95vw] w-full max-h-[80vh] overflow-y-auto p-4 sm:p-6">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title} - Metadata</DialogTitle>
|
<DialogTitle className="text-base sm:text-lg truncate pr-6">{title} - Metadata</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="mt-4 p-4 bg-muted/30 rounded-md">
|
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
|
||||||
<JsonView data={jsonData} style={defaultStyles} />
|
<JsonView data={jsonData} style={defaultStyles} />
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
@ -70,11 +70,11 @@ export function JsonMetadataViewer({
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-4xl max-w-[95vw] w-full max-h-[80vh] overflow-y-auto p-4 sm:p-6">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title} - Metadata</DialogTitle>
|
<DialogTitle className="text-base sm:text-lg truncate pr-6">{title} - Metadata</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="mt-4 p-4 bg-muted/30 rounded-md">
|
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
|
||||||
<JsonView data={jsonData} style={defaultStyles} />
|
<JsonView data={jsonData} style={defaultStyles} />
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -175,39 +175,41 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 gap-2 px-3 rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm",
|
"h-7 md:h-9 gap-1 md:gap-2 px-2 md:px-3 rounded-lg md:rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm",
|
||||||
"hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
|
"hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
|
||||||
"text-sm font-medium text-foreground",
|
"text-xs md:text-sm font-medium text-foreground",
|
||||||
"focus-visible:ring-0 focus-visible:ring-offset-0",
|
"focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
<Loader2 className="size-3.5 md:size-4 animate-spin text-muted-foreground" />
|
||||||
<span className="text-muted-foreground">Loading...</span>
|
<span className="text-muted-foreground hidden md:inline">Loading...</span>
|
||||||
|
<span className="text-muted-foreground md:hidden">Load...</span>
|
||||||
</>
|
</>
|
||||||
) : currentConfig ? (
|
) : currentConfig ? (
|
||||||
<>
|
<>
|
||||||
{getProviderIcon(currentConfig.provider)}
|
{getProviderIcon(currentConfig.provider)}
|
||||||
<span className="max-w-[150px] truncate">{currentConfig.name}</span>
|
<span className="max-w-[80px] md:max-w-[150px] truncate">{currentConfig.name}</span>
|
||||||
<Badge variant="secondary" className="ml-1 text-[10px] px-1.5 py-0 h-4 bg-muted/80">
|
<Badge variant="secondary" className="ml-0.5 md:ml-1 text-[9px] md:text-[10px] px-1 md:px-1.5 py-0 h-3.5 md:h-4 bg-muted/80">
|
||||||
{currentConfig.model_name.split("/").pop()?.slice(0, 15) ||
|
{currentConfig.model_name.split("/").pop()?.slice(0, 10) ||
|
||||||
currentConfig.model_name.slice(0, 15)}
|
currentConfig.model_name.slice(0, 10)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Bot className="size-4 text-muted-foreground" />
|
<Bot className="size-3.5 md:size-4 text-muted-foreground" />
|
||||||
<span className="text-muted-foreground">Select Model</span>
|
<span className="text-muted-foreground hidden md:inline">Select Model</span>
|
||||||
|
<span className="text-muted-foreground md:hidden">Model</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ChevronDown className="size-3.5 text-muted-foreground ml-1 shrink-0" />
|
<ChevronDown className="size-3 md:size-3.5 text-muted-foreground ml-1 shrink-0" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-[360px] p-0 rounded-xl shadow-lg border-border/30"
|
className="w-[280px] md:w-[360px] p-0 rounded-lg md:rounded-xl shadow-lg border-border/30"
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
|
|
@ -225,17 +227,17 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 border-b border-border/30 px-3 py-2">
|
<div className="flex items-center gap-1 md:gap-2 border-b border-border/30 px-2 md:px-3 py-1.5 md:py-2">
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search models..."
|
placeholder="Search models..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onValueChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
className="h-8 border-0 bg-transparent focus:ring-0 placeholder:text-muted-foreground/60"
|
className="h-7 md:h-8 text-xs md:text-sm border-0 bg-transparent focus:ring-0 placeholder:text-muted-foreground/60"
|
||||||
disabled={isSwitching}
|
disabled={isSwitching}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommandList className="max-h-[400px] overflow-y-auto">
|
<CommandList className="max-h-[300px] md:max-h-[400px] overflow-y-auto">
|
||||||
<CommandEmpty className="py-8 text-center">
|
<CommandEmpty className="py-8 text-center">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Bot className="size-8 text-muted-foreground/40" />
|
<Bot className="size-8 text-muted-foreground/40" />
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,10 @@ interface AllChatsSidebarProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
|
onCloseMobileSidebar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsSidebarProps) {
|
export function AllChatsSidebar({ open, onOpenChange, searchSpaceId, onCloseMobileSidebar }: AllChatsSidebarProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -61,6 +62,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||||
|
|
||||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||||
|
|
@ -120,8 +122,10 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
(threadId: number) => {
|
(threadId: number) => {
|
||||||
router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`);
|
router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
// Also close the main sidebar on mobile
|
||||||
|
onCloseMobileSidebar?.();
|
||||||
},
|
},
|
||||||
[router, onOpenChange, searchSpaceId]
|
[router, onOpenChange, searchSpaceId, onCloseMobileSidebar]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle thread deletion
|
// Handle thread deletion
|
||||||
|
|
@ -209,7 +213,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="fixed inset-0 z-50 bg-black/50"
|
className="fixed inset-0 z-[70] bg-black/50"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
@ -220,7 +224,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: "-100%" }}
|
exit={{ x: "-100%" }}
|
||||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||||
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
|
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={t("all_chats") || "All Chats"}
|
aria-label={t("all_chats") || "All Chats"}
|
||||||
|
|
@ -345,14 +349,17 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Actions dropdown */}
|
{/* Actions dropdown */}
|
||||||
<DropdownMenu>
|
<DropdownMenu
|
||||||
|
open={openDropdownId === thread.id}
|
||||||
|
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-6 w-6 shrink-0",
|
"h-6 w-6 shrink-0",
|
||||||
"opacity-0 group-hover:opacity-100 focus:opacity-100",
|
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
||||||
"transition-opacity"
|
"transition-opacity"
|
||||||
)}
|
)}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
|
|
@ -365,7 +372,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-40">
|
<DropdownMenuContent align="end" className="w-40 z-[80]">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||||
disabled={isArchiving}
|
disabled={isArchiving}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ interface AllNotesSidebarProps {
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
onAddNote?: () => void;
|
onAddNote?: () => void;
|
||||||
|
onCloseMobileSidebar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AllNotesSidebar({
|
export function AllNotesSidebar({
|
||||||
|
|
@ -34,6 +35,7 @@ export function AllNotesSidebar({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
onAddNote,
|
onAddNote,
|
||||||
|
onCloseMobileSidebar,
|
||||||
}: AllNotesSidebarProps) {
|
}: AllNotesSidebarProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -45,6 +47,7 @@ export function AllNotesSidebar({
|
||||||
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||||
|
|
||||||
// Handle mounting for portal
|
// Handle mounting for portal
|
||||||
|
|
@ -114,8 +117,10 @@ export function AllNotesSidebar({
|
||||||
(noteId: number, noteSearchSpaceId: number) => {
|
(noteId: number, noteSearchSpaceId: number) => {
|
||||||
router.push(`/dashboard/${noteSearchSpaceId}/editor/${noteId}`);
|
router.push(`/dashboard/${noteSearchSpaceId}/editor/${noteId}`);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
// Also close the main sidebar on mobile
|
||||||
|
onCloseMobileSidebar?.();
|
||||||
},
|
},
|
||||||
[router, onOpenChange]
|
[router, onOpenChange, onCloseMobileSidebar]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle note deletion
|
// Handle note deletion
|
||||||
|
|
@ -195,7 +200,7 @@ export function AllNotesSidebar({
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="fixed inset-0 z-50 bg-black/50"
|
className="fixed inset-0 z-[70] bg-black/50"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
@ -206,7 +211,7 @@ export function AllNotesSidebar({
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: "-100%" }}
|
exit={{ x: "-100%" }}
|
||||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||||
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
|
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={t("all_notes") || "All Notes"}
|
aria-label={t("all_notes") || "All Notes"}
|
||||||
|
|
@ -307,14 +312,17 @@ export function AllNotesSidebar({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Actions dropdown - separate from main click area */}
|
{/* Actions dropdown - separate from main click area */}
|
||||||
<DropdownMenu>
|
<DropdownMenu
|
||||||
|
open={openDropdownId === note.id}
|
||||||
|
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? note.id : null)}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-6 w-6 shrink-0",
|
"h-6 w-6 shrink-0",
|
||||||
"opacity-0 group-hover:opacity-100 focus:opacity-100",
|
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
||||||
"transition-opacity"
|
"transition-opacity"
|
||||||
)}
|
)}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
|
|
@ -327,7 +335,7 @@ export function AllNotesSidebar({
|
||||||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-40">
|
<DropdownMenuContent align="end" className="w-40 z-[80]">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleDeleteNote(note.id, note.search_space_id)}
|
onClick={() => handleDeleteNote(note.id, note.search_space_id)}
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -73,6 +74,7 @@ export function NavChats({
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { setOpenMobile } = useSidebar();
|
||||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
|
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
|
||||||
|
|
@ -119,7 +121,7 @@ export function NavChats({
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
{/* Action buttons - always visible on hover */}
|
{/* Action buttons - always visible on hover */}
|
||||||
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
|
<div className="flex items-center gap-0.5 md:opacity-0 md:group-hover/header:opacity-100 transition-opacity pr-1">
|
||||||
{searchSpaceId && chats.length > 0 && (
|
{searchSpaceId && chats.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -171,7 +173,7 @@ export function NavChats({
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-6 w-6",
|
"h-6 w-6",
|
||||||
"opacity-0 group-hover/chat:opacity-100 focus:opacity-100",
|
"md:opacity-0 md:group-hover/chat:opacity-100 md:focus:opacity-100",
|
||||||
"data-[state=open]:opacity-100",
|
"data-[state=open]:opacity-100",
|
||||||
"transition-opacity"
|
"transition-opacity"
|
||||||
)}
|
)}
|
||||||
|
|
@ -242,6 +244,7 @@ export function NavChats({
|
||||||
open={isAllChatsSidebarOpen}
|
open={isAllChatsSidebarOpen}
|
||||||
onOpenChange={setIsAllChatsSidebarOpen}
|
onOpenChange={setIsAllChatsSidebarOpen}
|
||||||
searchSpaceId={searchSpaceId}
|
searchSpaceId={searchSpaceId}
|
||||||
|
onCloseMobileSidebar={() => setOpenMobile(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useLogsSummary } from "@/hooks/use-logs";
|
import { useLogsSummary } from "@/hooks/use-logs";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
|
@ -75,6 +76,7 @@ export function NavNotes({
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { setOpenMobile } = useSidebar();
|
||||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
|
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
|
||||||
|
|
@ -136,7 +138,7 @@ export function NavNotes({
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
{/* Action buttons - always visible on hover */}
|
{/* Action buttons - always visible on hover */}
|
||||||
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
|
<div className="flex items-center gap-0.5 md:opacity-0 md:group-hover/header:opacity-100 transition-opacity pr-1">
|
||||||
{searchSpaceId && notes.length > 0 && (
|
{searchSpaceId && notes.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -207,7 +209,7 @@ export function NavNotes({
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-6 w-6",
|
"h-6 w-6",
|
||||||
"opacity-0 group-hover/note:opacity-100 focus:opacity-100",
|
"md:opacity-0 md:group-hover/note:opacity-100 md:focus:opacity-100",
|
||||||
"data-[state=open]:opacity-100",
|
"data-[state=open]:opacity-100",
|
||||||
"transition-opacity"
|
"transition-opacity"
|
||||||
)}
|
)}
|
||||||
|
|
@ -291,6 +293,7 @@ export function NavNotes({
|
||||||
onOpenChange={setIsAllNotesSidebarOpen}
|
onOpenChange={setIsAllNotesSidebarOpen}
|
||||||
searchSpaceId={searchSpaceId}
|
searchSpaceId={searchSpaceId}
|
||||||
onAddNote={onAddNote}
|
onAddNote={onAddNote}
|
||||||
|
onCloseMobileSidebar={() => setOpenMobile(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue