mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +02:00
Merge pull request #831 from AnishSarkar22/fix/ui
feat: multiple UI enhancements
This commit is contained in:
commit
47e6a7f29e
45 changed files with 2176 additions and 2209 deletions
|
|
@ -373,10 +373,11 @@ async def read_documents(
|
||||||
# Convert database objects to API-friendly format
|
# Convert database objects to API-friendly format
|
||||||
api_documents = []
|
api_documents = []
|
||||||
for doc in db_documents:
|
for doc in db_documents:
|
||||||
# Get user name (display_name or email fallback)
|
|
||||||
created_by_name = None
|
created_by_name = None
|
||||||
|
created_by_email = None
|
||||||
if doc.created_by:
|
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
|
# Parse status from JSONB
|
||||||
status_data = None
|
status_data = None
|
||||||
|
|
@ -400,6 +401,7 @@ async def read_documents(
|
||||||
search_space_id=doc.search_space_id,
|
search_space_id=doc.search_space_id,
|
||||||
created_by_id=doc.created_by_id,
|
created_by_id=doc.created_by_id,
|
||||||
created_by_name=created_by_name,
|
created_by_name=created_by_name,
|
||||||
|
created_by_email=created_by_email,
|
||||||
status=status_data,
|
status=status_data,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -528,10 +530,11 @@ async def search_documents(
|
||||||
# Convert database objects to API-friendly format
|
# Convert database objects to API-friendly format
|
||||||
api_documents = []
|
api_documents = []
|
||||||
for doc in db_documents:
|
for doc in db_documents:
|
||||||
# Get user name (display_name or email fallback)
|
|
||||||
created_by_name = None
|
created_by_name = None
|
||||||
|
created_by_email = None
|
||||||
if doc.created_by:
|
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
|
# Parse status from JSONB
|
||||||
status_data = None
|
status_data = None
|
||||||
|
|
@ -555,6 +558,7 @@ async def search_documents(
|
||||||
search_space_id=doc.search_space_id,
|
search_space_id=doc.search_space_id,
|
||||||
created_by_id=doc.created_by_id,
|
created_by_id=doc.created_by_id,
|
||||||
created_by_name=created_by_name,
|
created_by_name=created_by_name,
|
||||||
|
created_by_email=created_by_email,
|
||||||
status=status_data,
|
status=status_data,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,8 @@ class DocumentRead(BaseModel):
|
||||||
updated_at: datetime | None
|
updated_at: datetime | None
|
||||||
search_space_id: int
|
search_space_id: int
|
||||||
created_by_id: UUID | None = None # User who created/uploaded this document
|
created_by_id: UUID | None = None # User who created/uploaded this document
|
||||||
created_by_name: str | None = (
|
created_by_name: str | None = None
|
||||||
None # Display name or email of the user who created this document
|
created_by_email: str | None = None
|
||||||
)
|
|
||||||
status: DocumentStatusSchema | None = (
|
status: DocumentStatusSchema | None = (
|
||||||
None # Processing status (ready, processing, failed)
|
None # Processing status (ready, processing, failed)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export function DocumentTypeChip({ type, className }: { type: string; className?
|
||||||
|
|
||||||
const chip = (
|
const chip = (
|
||||||
<span
|
<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 className="flex-shrink-0">{icon}</span>
|
||||||
<span ref={textRef} className="truncate min-w-0">
|
<span ref={textRef} className="truncate min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
CircleX,
|
|
||||||
FilePlus2,
|
|
||||||
FileType,
|
FileType,
|
||||||
ListFilter,
|
ListFilter,
|
||||||
Search,
|
Search,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Trash,
|
Trash,
|
||||||
|
Upload,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
@ -81,7 +81,7 @@ export function DocumentsFilters({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4 select-none"
|
||||||
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 }}
|
||||||
|
|
@ -96,7 +96,7 @@ export function DocumentsFilters({
|
||||||
size="sm"
|
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"
|
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>
|
<span>Upload documents</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -126,7 +126,7 @@ export function DocumentsFilters({
|
||||||
<Input
|
<Input
|
||||||
id={`${id}-input`}
|
id={`${id}-input`}
|
||||||
ref={inputRef}
|
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}
|
value={searchValue}
|
||||||
onChange={(e) => onSearch(e.target.value)}
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
placeholder="Filter by title"
|
placeholder="Filter by title"
|
||||||
|
|
@ -135,7 +135,7 @@ export function DocumentsFilters({
|
||||||
/>
|
/>
|
||||||
{Boolean(searchValue) && (
|
{Boolean(searchValue) && (
|
||||||
<motion.button
|
<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"
|
aria-label="Clear filter"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSearch("");
|
onSearch("");
|
||||||
|
|
@ -147,7 +147,7 @@ export function DocumentsFilters({
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
>
|
>
|
||||||
<CircleX size={14} strokeWidth={2} aria-hidden="true" />
|
<X size={14} strokeWidth={2} aria-hidden="true" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ export function DocumentsTableShell({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<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 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.2 }}
|
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.2 }}
|
||||||
|
|
@ -453,7 +453,7 @@ export function DocumentsTableShell({
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="flex h-[50vh] w-full items-center justify-center">
|
<div className="flex h-[50vh] w-full items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<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>
|
<p className="text-sm text-destructive">{t("error_loading")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -482,7 +482,7 @@ export function DocumentsTableShell({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Desktop Table View - Notion Style */}
|
{/* Desktop Table View */}
|
||||||
<div className="hidden md:flex md:flex-col">
|
<div className="hidden md:flex md:flex-col">
|
||||||
{/* Fixed Header */}
|
{/* Fixed Header */}
|
||||||
<Table className="table-fixed w-full">
|
<Table className="table-fixed w-full">
|
||||||
|
|
@ -629,7 +629,24 @@ export function DocumentsTableShell({
|
||||||
)}
|
)}
|
||||||
{columnVisibility.created_by && (
|
{columnVisibility.created_by && (
|
||||||
<TableCell className="w-36 py-2.5 text-sm text-foreground truncate border-r border-border/40">
|
<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>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{columnVisibility.created_at && (
|
{columnVisibility.created_at && (
|
||||||
|
|
@ -765,11 +782,11 @@ export function DocumentsTableShell({
|
||||||
|
|
||||||
{/* Document Content Viewer - lazy loads content on-demand */}
|
{/* Document Content Viewer - lazy loads content on-demand */}
|
||||||
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col overflow-hidden pb-0">
|
||||||
<DialogHeader>
|
<DialogHeader className="flex-shrink-0">
|
||||||
<DialogTitle>{viewingDoc?.title}</DialogTitle>
|
<DialogTitle>{viewingDoc?.title}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="mt-4">
|
<div className="mt-4 overflow-y-auto flex-1 min-h-0 px-6 select-text">
|
||||||
{viewingLoading ? (
|
{viewingLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Spinner size="lg" className="text-muted-foreground" />
|
<Spinner size="lg" className="text-muted-foreground" />
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function PaginationControls({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<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 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.3 }}
|
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.3 }}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export type Document = {
|
||||||
search_space_id: number;
|
search_space_id: number;
|
||||||
created_by_id?: string | null;
|
created_by_id?: string | null;
|
||||||
created_by_name?: string | null;
|
created_by_name?: string | null;
|
||||||
|
created_by_email?: string | null;
|
||||||
status?: DocumentStatus;
|
status?: DocumentStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ export default function DocumentsTable() {
|
||||||
title: item.title,
|
title: item.title,
|
||||||
created_by_id: item.created_by_id ?? null,
|
created_by_id: item.created_by_id ?? null,
|
||||||
created_by_name: item.created_by_name ?? null,
|
created_by_name: item.created_by_name ?? null,
|
||||||
|
created_by_email: item.created_by_email ?? null,
|
||||||
created_at: item.created_at,
|
created_at: item.created_at,
|
||||||
status: (
|
status: (
|
||||||
item as {
|
item as {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
CircleX,
|
|
||||||
Clock,
|
Clock,
|
||||||
Columns3,
|
Columns3,
|
||||||
Filter,
|
Filter,
|
||||||
|
|
@ -741,7 +740,7 @@ function LogsFilters({
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircleX size={16} strokeWidth={2} />
|
<X size={16} strokeWidth={2} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export default function MorePagesPage() {
|
||||||
const allCompleted = data?.tasks.every((t) => t.completed) ?? false;
|
const allCompleted = data?.tasks.every((t) => t.completed) ?? false;
|
||||||
|
|
||||||
return (
|
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
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|
@ -174,7 +174,7 @@ export default function MorePagesPage() {
|
||||||
Contact Us
|
Contact Us
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="select-none sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Contact Us</DialogTitle>
|
<DialogTitle>Contact Us</DialogTitle>
|
||||||
<DialogDescription>Schedule a meeting or send us an email.</DialogDescription>
|
<DialogDescription>Schedule a meeting or send us an email.</DialogDescription>
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ export default function OnboardPage() {
|
||||||
You can add more configurations and customize settings anytime in{" "}
|
You can add more configurations and customize settings anytime in{" "}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings`)}
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings?section=general`)}
|
||||||
className="text-violet-500 hover:underline"
|
className="text-violet-500 hover:underline"
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@ import {
|
||||||
Menu,
|
Menu,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Settings,
|
Settings,
|
||||||
|
Shield,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/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 { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { PublicChatSnapshotsManager } from "@/components/public-chat-snapshots/public-chat-snapshots-manager";
|
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 { LLMRoleManager } from "@/components/settings/llm-role-manager";
|
||||||
import { ModelConfigManager } from "@/components/settings/model-config-manager";
|
import { ModelConfigManager } from "@/components/settings/model-config-manager";
|
||||||
import { PromptConfigManager } from "@/components/settings/prompt-config-manager";
|
import { PromptConfigManager } from "@/components/settings/prompt-config-manager";
|
||||||
|
import { RolesManager } from "@/components/settings/roles-manager";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { trackSettingsViewed } from "@/lib/posthog/events";
|
import { trackSettingsViewed } from "@/lib/posthog/events";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -72,6 +74,12 @@ const settingsNavItems: SettingsNavItem[] = [
|
||||||
descriptionKey: "nav_public_links_desc",
|
descriptionKey: "nav_public_links_desc",
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "team-roles",
|
||||||
|
labelKey: "nav_team_roles",
|
||||||
|
descriptionKey: "nav_team_roles_desc",
|
||||||
|
icon: Shield,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function SettingsSidebar({
|
function SettingsSidebar({
|
||||||
|
|
@ -240,7 +248,7 @@ function SettingsContent({
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
key={activeSection + "-header"}
|
key={`${activeSection}-header`}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -10 }}
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
|
@ -298,6 +306,7 @@ function SettingsContent({
|
||||||
{activeSection === "public-links" && (
|
{activeSection === "public-links" && (
|
||||||
<PublicChatSnapshotsManager searchSpaceId={searchSpaceId} />
|
<PublicChatSnapshotsManager searchSpaceId={searchSpaceId} />
|
||||||
)}
|
)}
|
||||||
|
{activeSection === "team-roles" && <RolesManager searchSpaceId={searchSpaceId} />}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</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() {
|
export default function SettingsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const searchSpaceId = Number(params.search_space_id);
|
const searchSpaceId = Number(params.search_space_id);
|
||||||
const [activeSection, setActiveSection] = useState("general");
|
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
trackSettingsViewed(searchSpaceId, activeSection);
|
trackSettingsViewed(searchSpaceId, activeSection);
|
||||||
}, [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">
|
<div className="flex h-full w-full overflow-hidden bg-background md:rounded-xl md:border md:shadow-sm">
|
||||||
<SettingsSidebar
|
<SettingsSidebar
|
||||||
activeSection={activeSection}
|
activeSection={activeSection}
|
||||||
onSectionChange={setActiveSection}
|
onSectionChange={handleSectionChange}
|
||||||
onBackToApp={handleBackToApp}
|
onBackToApp={handleBackToApp}
|
||||||
isOpen={isSidebarOpen}
|
isOpen={isSidebarOpen}
|
||||||
onClose={() => setIsSidebarOpen(false)}
|
onClose={() => setIsSidebarOpen(false)}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -221,7 +221,7 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger
|
||||||
</TooltipIconButton>
|
</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>
|
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
|
||||||
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
||||||
{isYouTubeView && searchSpaceId ? (
|
{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."}
|
: "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>
|
</p>
|
||||||
<Button asChild size="sm" variant="outline">
|
<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" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Go to Settings
|
Go to Settings
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ const DocumentUploadPopupContent: FC<{
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<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>
|
<DialogTitle className="sr-only">Upload Document</DialogTitle>
|
||||||
|
|
||||||
{/* Scrollable container for mobile */}
|
{/* 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">
|
<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 */}
|
{/* Upload header */}
|
||||||
<div className="flex items-center gap-2 sm:gap-4 mb-2 sm:mb-6">
|
<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">
|
<div className="flex-1 min-w-0 pr-8 sm:pr-0">
|
||||||
<h2 className="text-base sm:text-2xl font-semibold tracking-tight">
|
<h2 className="text-base sm:text-2xl font-semibold tracking-tight">
|
||||||
Upload Documents
|
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."}
|
: "You need to configure a Document Summary LLM before uploading files. This LLM is used to process and summarize your uploaded documents."}
|
||||||
</p>
|
</p>
|
||||||
<Button asChild size="sm" variant="outline">
|
<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" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Go to Settings
|
Go to Settings
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
|
Dot,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
FileWarning,
|
FileWarning,
|
||||||
Paperclip,
|
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="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<TooltipIconButton
|
<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"
|
side="bottom"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Slottable } from "@radix-ui/react-slot";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {
|
export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {
|
||||||
tooltip: string;
|
tooltip: ReactNode;
|
||||||
side?: "top" | "bottom" | "left" | "right";
|
side?: "top" | "bottom" | "left" | "right";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ export function DashboardBreadcrumb() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumb>
|
<Breadcrumb className="select-none">
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
{breadcrumbs.map((item, index) => (
|
{breadcrumbs.map((item, index) => (
|
||||||
<React.Fragment key={`${index}-${item.href || item.label}`}>
|
<React.Fragment key={`${index}-${item.href || item.label}`}>
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -334,7 +334,7 @@ export function LayoutDataProvider({
|
||||||
|
|
||||||
const handleSearchSpaceSettings = useCallback(
|
const handleSearchSpaceSettings = useCallback(
|
||||||
(space: SearchSpace) => {
|
(space: SearchSpace) => {
|
||||||
router.push(`/dashboard/${space.id}/settings`);
|
router.push(`/dashboard/${space.id}/settings?section=general`);
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
@ -478,7 +478,7 @@ export function LayoutDataProvider({
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSettings = useCallback(() => {
|
const handleSettings = useCallback(() => {
|
||||||
router.push(`/dashboard/${searchSpaceId}/settings`);
|
router.push(`/dashboard/${searchSpaceId}/settings?section=general`);
|
||||||
}, [router, searchSpaceId]);
|
}, [router, searchSpaceId]);
|
||||||
|
|
||||||
const handleManageMembers = useCallback(() => {
|
const handleManageMembers = useCallback(() => {
|
||||||
|
|
@ -703,7 +703,6 @@ export function LayoutDataProvider({
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<PencilIcon className="h-5 w-5" />
|
|
||||||
<span>{tSidebar("rename_chat") || "Rename Chat"}</span>
|
<span>{tSidebar("rename_chat") || "Rename Chat"}</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|
@ -736,7 +735,7 @@ export function LayoutDataProvider({
|
||||||
{isRenamingChat ? (
|
{isRenamingChat ? (
|
||||||
<>
|
<>
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
{tSidebar("renaming") || "Renaming..."}
|
{tSidebar("renaming") || "Renaming"}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { Plus, Search } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
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">
|
<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">
|
<DialogHeader className="space-y-2 pb-2">
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<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">
|
<div className="flex-1 min-w-0">
|
||||||
<DialogTitle className="text-base sm:text-lg">{t("create_title")}</DialogTitle>
|
<DialogTitle className="text-base sm:text-lg">{t("create_title")}</DialogTitle>
|
||||||
<DialogDescription className="text-xs sm:text-sm mt-0.5">
|
<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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleOpenChange(false)}
|
onClick={() => handleOpenChange(false)}
|
||||||
disabled={isSubmitting}
|
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")}
|
{tCommon("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
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 ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -163,10 +159,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
||||||
{t("creating")}
|
{t("creating")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>{t("create_button")}</>
|
||||||
<Plus className="-mr-1 h-4 w-4" />
|
|
||||||
{t("create_button")}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,26 @@ export function LayoutShell({
|
||||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
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>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
|
ChevronLeft,
|
||||||
MessageCircleMore,
|
MessageCircleMore,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
|
PenLine,
|
||||||
RotateCcwIcon,
|
RotateCcwIcon,
|
||||||
Search,
|
Search,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
|
@ -17,6 +19,14 @@ import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -69,6 +79,10 @@ export function AllPrivateChatsSidebar({
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
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 debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||||
|
|
||||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||||
|
|
@ -187,6 +201,35 @@ export function AllPrivateChatsSidebar({
|
||||||
[queryClient, searchSpaceId, t]
|
[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(() => {
|
const handleClearSearch = useCallback(() => {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -205,6 +248,17 @@ export function AllPrivateChatsSidebar({
|
||||||
>
|
>
|
||||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<User className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -356,6 +410,14 @@ export function AllPrivateChatsSidebar({
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
<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
|
<DropdownMenuItem
|
||||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||||
disabled={isArchiving}
|
disabled={isArchiving}
|
||||||
|
|
@ -412,6 +474,51 @@ export function AllPrivateChatsSidebar({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</SidebarSlideOutPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
|
ChevronLeft,
|
||||||
MessageCircleMore,
|
MessageCircleMore,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
|
PenLine,
|
||||||
RotateCcwIcon,
|
RotateCcwIcon,
|
||||||
Search,
|
Search,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
|
@ -17,6 +19,14 @@ import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -69,6 +79,10 @@ export function AllSharedChatsSidebar({
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
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 debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||||
|
|
||||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||||
|
|
@ -187,6 +201,35 @@ export function AllSharedChatsSidebar({
|
||||||
[queryClient, searchSpaceId, t]
|
[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(() => {
|
const handleClearSearch = useCallback(() => {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -205,6 +248,17 @@ export function AllSharedChatsSidebar({
|
||||||
>
|
>
|
||||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<Users className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -356,6 +410,14 @@ export function AllSharedChatsSidebar({
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
<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
|
<DropdownMenuItem
|
||||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||||
disabled={isArchiving}
|
disabled={isArchiving}
|
||||||
|
|
@ -412,6 +474,51 @@ export function AllSharedChatsSidebar({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</SidebarSlideOutPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
PencilIcon,
|
PenLine,
|
||||||
RotateCcwIcon,
|
RotateCcwIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -74,7 +74,7 @@ export function ChatListItem({
|
||||||
onRename();
|
onRename();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="mr-2 h-4 w-4" />
|
<PenLine className="mr-2 h-4 w-4" />
|
||||||
<span>{t("rename") || "Rename"}</span>
|
<span>{t("rename") || "Rename"}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -727,7 +727,7 @@ export function InboxSidebar({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
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">
|
<DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal">
|
||||||
{t("filter") || "Filter"}
|
{t("filter") || "Filter"}
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export function MobileSidebar({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar Content - right side */}
|
{/* 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
|
<Sidebar
|
||||||
searchSpace={searchSpace}
|
searchSpace={searchSpace}
|
||||||
isCollapsed={false}
|
isCollapsed={false}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export function Sidebar({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 ? "w-[60px] transition-all duration-200" : "",
|
||||||
!isCollapsed && !isResizing ? "transition-all duration-200" : "",
|
!isCollapsed && !isResizing ? "transition-all duration-200" : "",
|
||||||
className
|
className
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronsUpDown, Settings, Users } from "lucide-react";
|
import { ChevronsUpDown, Settings, UserPen } from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -51,14 +51,14 @@ export function SidebarHeader({
|
||||||
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
<DropdownMenuContent align="start" className="w-48">
|
||||||
<DropdownMenuItem onClick={onManageMembers}>
|
<DropdownMenuItem onClick={onManageMembers}>
|
||||||
<Users className="mr-2 h-4 w-4" />
|
<UserPen className="h-4 w-4" />
|
||||||
{t("manage_members")}
|
{t("manage_members")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={onSettings}>
|
<DropdownMenuItem onClick={onSettings}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
{t("search_space_settings")}
|
{t("search_space_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export function SidebarSlideOutPanel({
|
||||||
exit={{ x: "-100%" }}
|
exit={{ x: "-100%" }}
|
||||||
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
transition={{ type: "tween", duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||||
className={cn(
|
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"
|
"sm:border-r sm:shadow-xl"
|
||||||
)}
|
)}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ export function SidebarUserProfile({
|
||||||
<TooltipContent side="right">{displayName}</TooltipContent>
|
<TooltipContent side="right">{displayName}</TooltipContent>
|
||||||
</Tooltip>
|
</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">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -191,14 +191,14 @@ export function SidebarUserProfile({
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onUserSettings}>
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{setTheme && (
|
{setTheme && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
<Sun className="h-4 w-4" />
|
||||||
{t("theme")}
|
{t("theme")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -216,7 +216,7 @@ export function SidebarUserProfile({
|
||||||
isSelected && "text-primary"
|
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>
|
<span className="flex-1">{t(themeOption.value)}</span>
|
||||||
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -229,7 +229,7 @@ export function SidebarUserProfile({
|
||||||
|
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Languages className="mr-2 h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
{t("language")}
|
{t("language")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -262,7 +262,7 @@ export function SidebarUserProfile({
|
||||||
{isLoggingOut ? (
|
{isLoggingOut ? (
|
||||||
<Spinner size="sm" className="mr-2" />
|
<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")}
|
{isLoggingOut ? t("loggingOut") : t("logout")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -299,7 +299,7 @@ export function SidebarUserProfile({
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</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">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -313,14 +313,14 @@ export function SidebarUserProfile({
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onUserSettings}>
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{setTheme && (
|
{setTheme && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
<Sun className="h-4 w-4" />
|
||||||
{t("theme")}
|
{t("theme")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -338,7 +338,7 @@ export function SidebarUserProfile({
|
||||||
isSelected && "text-primary"
|
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>
|
<span className="flex-1">{t(themeOption.value)}</span>
|
||||||
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -351,7 +351,7 @@ export function SidebarUserProfile({
|
||||||
|
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Languages className="mr-2 h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
{t("language")}
|
{t("language")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -381,11 +381,7 @@ export function SidebarUserProfile({
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
|
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
|
||||||
{isLoggingOut ? (
|
{isLoggingOut ? <Spinner size="sm" className="mr-2" /> : <LogOut className="h-4 w-4" />}
|
||||||
<Spinner size="sm" className="mr-2" />
|
|
||||||
) : (
|
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{isLoggingOut ? t("loggingOut") : t("logout")}
|
{isLoggingOut ? t("loggingOut") : t("logout")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,9 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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" />
|
<Earth className="h-4 w-4 text-muted-foreground" />
|
||||||
|
|
@ -181,7 +183,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<div className="p-1.5 space-y-1">
|
<div className="p-1.5 space-y-1 select-none">
|
||||||
{/* Visibility Options */}
|
{/* Visibility Options */}
|
||||||
{visibilityOptions.map((option) => {
|
{visibilityOptions.map((option) => {
|
||||||
const isSelected = currentVisibility === option.value;
|
const isSelected = currentVisibility === option.value;
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export function ModelSelector({
|
||||||
size="sm"
|
size="sm"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
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 ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -280,7 +280,7 @@ export function ModelSelector({
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent
|
<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"
|
align="start"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
|
|
@ -289,18 +289,18 @@ export function ModelSelector({
|
||||||
onValueChange={(v) => setActiveTab(v as "llm" | "image")}
|
onValueChange={(v) => setActiveTab(v as "llm" | "image")}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<div className="border-b border-border/40">
|
<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-card h-11 p-0 gap-0">
|
<TabsList className="w-full grid grid-cols-2 rounded-none rounded-t-lg bg-transparent h-11 p-0 gap-0">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="llm"
|
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" />
|
<Zap className="size-4" />
|
||||||
LLM
|
LLM
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="image"
|
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" />
|
<ImageIcon className="size-4" />
|
||||||
Image
|
Image
|
||||||
|
|
@ -312,7 +312,7 @@ export function ModelSelector({
|
||||||
<TabsContent value="llm" className="mt-0">
|
<TabsContent value="llm" className="mt-0">
|
||||||
<Command
|
<Command
|
||||||
shouldFilter={false}
|
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 && (
|
{totalLLMModels > 3 && (
|
||||||
<div className="px-2 md:px-3 py-1.5 md:py-2">
|
<div className="px-2 md:px-3 py-1.5 md:py-2">
|
||||||
|
|
@ -350,9 +350,9 @@ export function ModelSelector({
|
||||||
onSelect={() => handleSelectLLM(config)}
|
onSelect={() => handleSelectLLM(config)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
|
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
|
||||||
"hover:bg-accent/50",
|
"hover:bg-accent/50 dark:hover:bg-white/10",
|
||||||
isSelected && "bg-accent/80",
|
isSelected && "bg-accent/80 dark:bg-white/10",
|
||||||
isAutoMode && "border border-violet-800"
|
isAutoMode && ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full gap-2">
|
<div className="flex items-center justify-between w-full gap-2">
|
||||||
|
|
@ -366,7 +366,7 @@ export function ModelSelector({
|
||||||
{isAutoMode && (
|
{isAutoMode && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
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
|
Recommended
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -426,8 +426,8 @@ export function ModelSelector({
|
||||||
onSelect={() => handleSelectLLM(config)}
|
onSelect={() => handleSelectLLM(config)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
|
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all",
|
||||||
"hover:bg-accent/50",
|
"hover:bg-accent/50 dark:hover:bg-white/10",
|
||||||
isSelected && "bg-accent/80"
|
isSelected && "bg-accent/80 dark:bg-white/10"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full gap-2">
|
<div className="flex items-center justify-between w-full gap-2">
|
||||||
|
|
@ -471,11 +471,11 @@ export function ModelSelector({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add New LLM Config */}
|
{/* Add New LLM Config */}
|
||||||
<div className="p-2 bg-muted/20">
|
<div className="p-2 bg-muted/20 dark:bg-muted">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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={() => {
|
onClick={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
onAddNewLLM();
|
onAddNewLLM();
|
||||||
|
|
@ -493,7 +493,7 @@ export function ModelSelector({
|
||||||
<TabsContent value="image" className="mt-0">
|
<TabsContent value="image" className="mt-0">
|
||||||
<Command
|
<Command
|
||||||
shouldFilter={false}
|
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 && (
|
{totalImageModels > 3 && (
|
||||||
<div className="px-2 md:px-3 py-1.5 md:py-2">
|
<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}`}
|
value={`img-g-${config.id}`}
|
||||||
onSelect={() => handleSelectImage(config.id)}
|
onSelect={() => handleSelectImage(config.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
|
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50 dark:hover:bg-white/10",
|
||||||
isSelected && "bg-accent/80",
|
isSelected && "bg-accent/80 dark:bg-white/10",
|
||||||
isAuto && "border border-violet-800"
|
isAuto && ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
|
@ -543,7 +543,7 @@ export function ModelSelector({
|
||||||
{isAuto && (
|
{isAuto && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
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
|
Recommended
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -593,8 +593,8 @@ export function ModelSelector({
|
||||||
value={`img-u-${config.id}`}
|
value={`img-u-${config.id}`}
|
||||||
onSelect={() => handleSelectImage(config.id)}
|
onSelect={() => handleSelectImage(config.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
|
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50 dark:hover:bg-white/10",
|
||||||
isSelected && "bg-accent/80"
|
isSelected && "bg-accent/80 dark:bg-white/10"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
|
@ -634,11 +634,11 @@ export function ModelSelector({
|
||||||
|
|
||||||
{/* Add New Image Config */}
|
{/* Add New Image Config */}
|
||||||
{onAddNewImage && (
|
{onAddNewImage && (
|
||||||
<div className="p-2 bg-muted/20">
|
<div className="p-2 bg-muted/20 dark:bg-muted">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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={() => {
|
onClick={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
onAddNewImage();
|
onAddNewImage();
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,12 @@ export function PublicChatSnapshotRow({
|
||||||
memberMap,
|
memberMap,
|
||||||
}: PublicChatSnapshotRowProps) {
|
}: PublicChatSnapshotRowProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||||
|
|
||||||
const handleCopyClick = useCallback(() => {
|
const handleCopyClick = useCallback(() => {
|
||||||
onCopy(snapshot);
|
onCopy(snapshot);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
clearTimeout(copyTimeoutRef.current);
|
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
|
||||||
copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
||||||
}, [onCopy, snapshot]);
|
}, [onCopy, snapshot]);
|
||||||
|
|
||||||
|
|
@ -117,12 +117,14 @@ export function PublicChatSnapshotRow({
|
||||||
|
|
||||||
{/* Public URL – selectable fallback for manual copy */}
|
{/* 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">
|
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
|
||||||
<p
|
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
|
||||||
className="min-w-0 flex-1 text-[10px] font-mono text-muted-foreground break-all select-all cursor-text"
|
<p
|
||||||
title={snapshot.public_url}
|
className="text-[10px] font-mono text-muted-foreground whitespace-nowrap select-all cursor-text"
|
||||||
>
|
title={snapshot.public_url}
|
||||||
{snapshot.public_url}
|
>
|
||||||
</p>
|
{snapshot.public_url}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,7 @@ function ReportPanelContent({
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
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")}>
|
<DropdownMenuItem onClick={() => handleExport("md")}>
|
||||||
Download Markdown
|
Download Markdown
|
||||||
|
|
@ -361,56 +361,30 @@ function ReportPanelContent({
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
{/* Version switcher — only shown when multiple versions exist */}
|
{/* Version switcher — only shown when multiple versions exist */}
|
||||||
{versions.length > 1 &&
|
{versions.length > 1 && (
|
||||||
(insideDrawer ? (
|
<DropdownMenu modal={insideDrawer ? false : undefined}>
|
||||||
/* Mobile: compact dropdown */
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenu modal={false}>
|
<Button variant="outline" size="sm" className="h-8 px-3.5 py-4 text-[15px] gap-1.5">
|
||||||
<DropdownMenuTrigger asChild>
|
v{activeVersionIndex + 1}
|
||||||
<Button
|
<ChevronDownIcon className="size-3" />
|
||||||
variant="outline"
|
</Button>
|
||||||
size="sm"
|
</DropdownMenuTrigger>
|
||||||
className="h-8 px-3.5 py-4 text-[15px] gap-1.5"
|
<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}
|
Version {i + 1}
|
||||||
<ChevronDownIcon className="size-3" />
|
</DropdownMenuItem>
|
||||||
</Button>
|
))}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuContent>
|
||||||
<DropdownMenuContent align="start" className="min-w-[120px] z-[100]">
|
</DropdownMenu>
|
||||||
{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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">
|
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">
|
||||||
|
|
|
||||||
1132
surfsense_web/components/settings/roles-manager.tsx
Normal file
1132
surfsense_web/components/settings/roles-manager.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtom } from "jotai";
|
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 { AnimatePresence, motion } from "motion/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
|
@ -452,7 +452,6 @@ export function DocumentUploadTab({
|
||||||
<AccordionItem value="supported-file-types" className="border-0">
|
<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">
|
<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">
|
<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="text-left min-w-0">
|
||||||
<div className="font-semibold text-sm sm:text-base">
|
<div className="font-semibold text-sm sm:text-base">
|
||||||
{t("supported_file_types")}
|
{t("supported_file_types")}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export const document = z.object({
|
||||||
search_space_id: z.number(),
|
search_space_id: z.number(),
|
||||||
created_by_id: z.string().nullable().optional(),
|
created_by_id: z.string().nullable().optional(),
|
||||||
created_by_name: z.string().nullable().optional(),
|
created_by_name: z.string().nullable().optional(),
|
||||||
|
created_by_email: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const extensionDocumentContent = z.object({
|
export const extensionDocumentContent = z.object({
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ interface DocumentElectric {
|
||||||
status: DocumentStatusType | null;
|
status: DocumentStatusType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Document for display (with resolved user name)
|
// Document for display (with resolved user name and email)
|
||||||
export interface DocumentDisplay {
|
export interface DocumentDisplay {
|
||||||
id: number;
|
id: number;
|
||||||
search_space_id: number;
|
search_space_id: number;
|
||||||
|
|
@ -34,6 +34,7 @@ export interface DocumentDisplay {
|
||||||
title: string;
|
title: string;
|
||||||
created_by_id: string | null;
|
created_by_id: string | null;
|
||||||
created_by_name: string | null;
|
created_by_name: string | null;
|
||||||
|
created_by_email: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
status: DocumentStatusType;
|
status: DocumentStatusType;
|
||||||
}
|
}
|
||||||
|
|
@ -94,8 +95,9 @@ export function useDocuments(
|
||||||
// Track if initial API load is complete (source of truth)
|
// Track if initial API load is complete (source of truth)
|
||||||
const apiLoadedRef = useRef(false);
|
const apiLoadedRef = useRef(false);
|
||||||
|
|
||||||
// User cache: userId → displayName
|
// User cache: userId → displayName / email
|
||||||
const userCacheRef = useRef<Map<string, string>>(new Map());
|
const userCacheRef = useRef<Map<string, string>>(new Map());
|
||||||
|
const emailCacheRef = useRef<Map<string, string>>(new Map());
|
||||||
|
|
||||||
// Electric sync refs
|
// Electric sync refs
|
||||||
const syncHandleRef = useRef<SyncHandle | null>(null);
|
const syncHandleRef = useRef<SyncHandle | null>(null);
|
||||||
|
|
@ -119,10 +121,21 @@ export function useDocuments(
|
||||||
|
|
||||||
// Populate user cache from API response
|
// Populate user cache from API response
|
||||||
const populateUserCache = useCallback(
|
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) {
|
for (const item of items) {
|
||||||
if (item.created_by_id && item.created_by_name) {
|
if (item.created_by_id) {
|
||||||
userCacheRef.current.set(item.created_by_id, item.created_by_name);
|
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;
|
title: string;
|
||||||
created_by_id?: string | null;
|
created_by_id?: string | null;
|
||||||
created_by_name?: string | null;
|
created_by_name?: string | null;
|
||||||
|
created_by_email?: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
status?: DocumentStatusType | null;
|
status?: DocumentStatusType | null;
|
||||||
}): DocumentDisplay => ({
|
}): DocumentDisplay => ({
|
||||||
|
|
@ -147,6 +161,7 @@ export function useDocuments(
|
||||||
title: item.title,
|
title: item.title,
|
||||||
created_by_id: item.created_by_id ?? null,
|
created_by_id: item.created_by_id ?? null,
|
||||||
created_by_name: item.created_by_name ?? null,
|
created_by_name: item.created_by_name ?? null,
|
||||||
|
created_by_email: item.created_by_email ?? null,
|
||||||
created_at: item.created_at,
|
created_at: item.created_at,
|
||||||
status: item.status ?? { state: "ready" },
|
status: item.status ?? { state: "ready" },
|
||||||
}),
|
}),
|
||||||
|
|
@ -160,6 +175,9 @@ export function useDocuments(
|
||||||
created_by_name: doc.created_by_id
|
created_by_name: doc.created_by_id
|
||||||
? (userCacheRef.current.get(doc.created_by_id) ?? null)
|
? (userCacheRef.current.get(doc.created_by_id) ?? null)
|
||||||
: null,
|
: null,
|
||||||
|
created_by_email: doc.created_by_id
|
||||||
|
? (emailCacheRef.current.get(doc.created_by_id) ?? null)
|
||||||
|
: null,
|
||||||
status: doc.status ?? { state: "ready" },
|
status: doc.status ?? { state: "ready" },
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
|
|
@ -351,6 +369,9 @@ export function useDocuments(
|
||||||
created_by_name: doc.created_by_id
|
created_by_name: doc.created_by_id
|
||||||
? (userCacheRef.current.get(doc.created_by_id) ?? null)
|
? (userCacheRef.current.get(doc.created_by_id) ?? null)
|
||||||
: 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([]);
|
setAllDocuments([]);
|
||||||
apiLoadedRef.current = false;
|
apiLoadedRef.current = false;
|
||||||
userCacheRef.current.clear();
|
userCacheRef.current.clear();
|
||||||
|
emailCacheRef.current.clear();
|
||||||
}
|
}
|
||||||
prevSearchSpaceIdRef.current = searchSpaceId;
|
prevSearchSpaceIdRef.current = searchSpaceId;
|
||||||
}, [searchSpaceId]);
|
}, [searchSpaceId]);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export function getProviderIcon(
|
||||||
{ isAutoMode, className = "size-4" }: { isAutoMode?: boolean; className?: string } = {}
|
{ isAutoMode, className = "size-4" }: { isAutoMode?: boolean; className?: string } = {}
|
||||||
) {
|
) {
|
||||||
if (isAutoMode || provider?.toUpperCase() === "AUTO") {
|
if (isAutoMode || provider?.toUpperCase() === "AUTO") {
|
||||||
return <Shuffle className={cn(className, "text-violet-800")} />;
|
return <Shuffle className={cn(className, "text-muted-foreground")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (provider?.toUpperCase()) {
|
switch (provider?.toUpperCase()) {
|
||||||
|
|
|
||||||
|
|
@ -682,7 +682,7 @@
|
||||||
"rename_chat": "Rename Chat",
|
"rename_chat": "Rename Chat",
|
||||||
"rename_chat_description": "Enter a new name for this conversation.",
|
"rename_chat_description": "Enter a new name for this conversation.",
|
||||||
"chat_title_placeholder": "Chat title",
|
"chat_title_placeholder": "Chat title",
|
||||||
"renaming": "Renaming...",
|
"renaming": "Renaming",
|
||||||
"no_archived_chats": "No archived chats",
|
"no_archived_chats": "No archived chats",
|
||||||
"error_archiving_chat": "Failed to archive chat",
|
"error_archiving_chat": "Failed to archive chat",
|
||||||
"new_chat": "New chat",
|
"new_chat": "New chat",
|
||||||
|
|
@ -720,7 +720,8 @@
|
||||||
"unread": "Unread",
|
"unread": "Unread",
|
||||||
"connectors": "Connectors",
|
"connectors": "Connectors",
|
||||||
"all_connectors": "All connectors",
|
"all_connectors": "All connectors",
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"cancel": "Cancel"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
|
|
@ -746,6 +747,8 @@
|
||||||
"nav_system_instructions_desc": "SearchSpace-wide AI instructions",
|
"nav_system_instructions_desc": "SearchSpace-wide AI instructions",
|
||||||
"nav_public_links": "Public Chat Links",
|
"nav_public_links": "Public Chat Links",
|
||||||
"nav_public_links_desc": "Manage publicly shared 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_label": "Name",
|
||||||
"general_name_placeholder": "Enter search space name",
|
"general_name_placeholder": "Enter search space name",
|
||||||
"general_name_description": "A unique name for your search space.",
|
"general_name_description": "A unique name for your search space.",
|
||||||
|
|
|
||||||
|
|
@ -720,7 +720,8 @@
|
||||||
"unread": "No leído",
|
"unread": "No leído",
|
||||||
"connectors": "Conectores",
|
"connectors": "Conectores",
|
||||||
"all_connectors": "Todos los conectores",
|
"all_connectors": "Todos los conectores",
|
||||||
"close": "Cerrar"
|
"close": "Cerrar",
|
||||||
|
"cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"something_went_wrong": "Algo salió mal",
|
"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_system_instructions_desc": "Instrucciones de IA a nivel del espacio de búsqueda",
|
||||||
"nav_public_links": "Enlaces de chat públicos",
|
"nav_public_links": "Enlaces de chat públicos",
|
||||||
"nav_public_links_desc": "Administrar enlaces de chat compartidos públicamente",
|
"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_label": "Nombre",
|
||||||
"general_name_placeholder": "Ingresa el nombre del espacio de búsqueda",
|
"general_name_placeholder": "Ingresa el nombre del espacio de búsqueda",
|
||||||
"general_name_description": "Un nombre único para tu espacio de búsqueda.",
|
"general_name_description": "Un nombre único para tu espacio de búsqueda.",
|
||||||
|
|
|
||||||
|
|
@ -720,7 +720,8 @@
|
||||||
"unread": "अपठित",
|
"unread": "अपठित",
|
||||||
"connectors": "कनेक्टर",
|
"connectors": "कनेक्टर",
|
||||||
"all_connectors": "सभी कनेक्टर",
|
"all_connectors": "सभी कनेक्टर",
|
||||||
"close": "बंद करें"
|
"close": "बंद करें",
|
||||||
|
"cancel": "रद्द करें"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"something_went_wrong": "कुछ गलत हो गया",
|
"something_went_wrong": "कुछ गलत हो गया",
|
||||||
|
|
@ -746,6 +747,8 @@
|
||||||
"nav_system_instructions_desc": "सर्च स्पेस-व्यापी AI निर्देश",
|
"nav_system_instructions_desc": "सर्च स्पेस-व्यापी AI निर्देश",
|
||||||
"nav_public_links": "सार्वजनिक चैट लिंक",
|
"nav_public_links": "सार्वजनिक चैट लिंक",
|
||||||
"nav_public_links_desc": "सार्वजनिक रूप से साझा किए गए चैट लिंक प्रबंधित करें",
|
"nav_public_links_desc": "सार्वजनिक रूप से साझा किए गए चैट लिंक प्रबंधित करें",
|
||||||
|
"nav_team_roles": "Team Roles",
|
||||||
|
"nav_team_roles_desc": "Manage team roles & permissions",
|
||||||
"general_name_label": "नाम",
|
"general_name_label": "नाम",
|
||||||
"general_name_placeholder": "सर्च स्पेस का नाम दर्ज करें",
|
"general_name_placeholder": "सर्च स्पेस का नाम दर्ज करें",
|
||||||
"general_name_description": "आपके सर्च स्पेस के लिए एक अद्वितीय नाम।",
|
"general_name_description": "आपके सर्च स्पेस के लिए एक अद्वितीय नाम।",
|
||||||
|
|
|
||||||
|
|
@ -720,7 +720,8 @@
|
||||||
"unread": "Não lido",
|
"unread": "Não lido",
|
||||||
"connectors": "Conectores",
|
"connectors": "Conectores",
|
||||||
"all_connectors": "Todos os conectores",
|
"all_connectors": "Todos os conectores",
|
||||||
"close": "Fechar"
|
"close": "Fechar",
|
||||||
|
"cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"something_went_wrong": "Algo deu errado",
|
"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_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": "Links de chat públicos",
|
||||||
"nav_public_links_desc": "Gerenciar links de chat compartilhados publicamente",
|
"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_label": "Nome",
|
||||||
"general_name_placeholder": "Insira o nome do espaço de pesquisa",
|
"general_name_placeholder": "Insira o nome do espaço de pesquisa",
|
||||||
"general_name_description": "Um nome único para seu espaço de pesquisa.",
|
"general_name_description": "Um nome único para seu espaço de pesquisa.",
|
||||||
|
|
|
||||||
|
|
@ -704,7 +704,8 @@
|
||||||
"unread": "未读",
|
"unread": "未读",
|
||||||
"connectors": "连接器",
|
"connectors": "连接器",
|
||||||
"all_connectors": "所有连接器",
|
"all_connectors": "所有连接器",
|
||||||
"close": "关闭"
|
"close": "关闭",
|
||||||
|
"cancel": "取消"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"something_went_wrong": "出错了",
|
"something_went_wrong": "出错了",
|
||||||
|
|
@ -730,6 +731,8 @@
|
||||||
"nav_system_instructions_desc": "搜索空间级别的 AI 指令",
|
"nav_system_instructions_desc": "搜索空间级别的 AI 指令",
|
||||||
"nav_public_links": "公开聊天链接",
|
"nav_public_links": "公开聊天链接",
|
||||||
"nav_public_links_desc": "管理公开分享的聊天链接",
|
"nav_public_links_desc": "管理公开分享的聊天链接",
|
||||||
|
"nav_team_roles": "Team Roles",
|
||||||
|
"nav_team_roles_desc": "Manage team roles & permissions",
|
||||||
"general_name_label": "名称",
|
"general_name_label": "名称",
|
||||||
"general_name_placeholder": "输入搜索空间名称",
|
"general_name_placeholder": "输入搜索空间名称",
|
||||||
"general_name_description": "您的搜索空间的唯一名称。",
|
"general_name_description": "您的搜索空间的唯一名称。",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue