refactor(ux): Update dashboard links and enhance settings page layout

- Changed document links to point to the researcher page in the dashboard.
- Removed unused team and settings links from the dashboard layout.
- Redesigned settings page with a sidebar for navigation and improved layout for better user experience.
- Added delete confirmation dialog for model configurations in the settings manager.
- Updated card components for better visual consistency and responsiveness.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-13 22:43:38 -08:00
parent 71465398db
commit 3a3712ceac
9 changed files with 720 additions and 297 deletions

View file

@ -0,0 +1,9 @@
import type React from "react";
/**
* Settings layout - renders children directly without the parent sidebar
* This creates a full-screen settings experience
*/
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
return <div className="fixed inset-0 z-50 bg-background">{children}</div>;
}

View file

@ -1,83 +1,302 @@
"use client";
import { ArrowLeft, Bot, Brain, MessageSquare, Settings } from "lucide-react";
import {
ArrowLeft,
Bot,
Brain,
ChevronRight,
type LucideIcon,
Menu,
MessageSquare,
Settings,
X,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { LLMRoleManager } from "@/components/settings/llm-role-manager";
import { ModelConfigManager } from "@/components/settings/model-config-manager";
import { PromptConfigManager } from "@/components/settings/prompt-config-manager";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface SettingsNavItem {
id: string;
label: string;
description: string;
icon: LucideIcon;
}
const settingsNavItems: SettingsNavItem[] = [
{
id: "models",
label: "Model Configs",
description: "Configure AI models and providers",
icon: Bot,
},
{
id: "roles",
label: "LLM Roles",
description: "Manage language model roles",
icon: Brain,
},
{
id: "prompts",
label: "System Instructions",
description: "Customize system prompts",
icon: MessageSquare,
},
];
function SettingsSidebar({
activeSection,
onSectionChange,
onBackToApp,
isOpen,
onClose,
}: {
activeSection: string;
onSectionChange: (section: string) => void;
onBackToApp: () => void;
isOpen: boolean;
onClose: () => void;
}) {
const handleNavClick = (sectionId: string) => {
onSectionChange(sectionId);
onClose(); // Close sidebar on mobile after selection
};
return (
<>
{/* Mobile overlay */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40 md:hidden"
onClick={onClose}
/>
)}
</AnimatePresence>
{/* Sidebar */}
<aside
className={cn(
"fixed md:relative left-0 top-0 z-50 md:z-auto",
"w-72 shrink-0 border-r border-border bg-background md:bg-muted/20 h-full flex flex-col",
"transition-transform duration-300 ease-out",
"md:translate-x-0",
isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
)}
>
{/* Header with back button */}
<div className="p-4 border-b border-border flex items-center justify-between">
<Button
variant="ghost"
onClick={onBackToApp}
className="flex-1 justify-start gap-3 h-11 px-3 hover:bg-muted group"
>
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 group-hover:bg-primary/20 transition-colors">
<ArrowLeft className="h-4 w-4 text-primary" />
</div>
<span className="font-medium">Back to app</span>
</Button>
{/* Mobile close button */}
<Button variant="ghost" size="icon" onClick={onClose} className="md:hidden h-9 w-9">
<X className="h-5 w-5" />
</Button>
</div>
{/* Navigation Items */}
<nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto">
{settingsNavItems.map((item, index) => {
const isActive = activeSection === item.id;
const Icon = item.icon;
return (
<motion.button
key={item.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 + index * 0.05, duration: 0.3 }}
onClick={() => handleNavClick(item.id)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className={cn(
"relative w-full flex items-center gap-3 px-3 py-3 rounded-xl text-left transition-all duration-200",
isActive ? "bg-muted shadow-sm border border-border" : "hover:bg-muted/60"
)}
>
{isActive && (
<motion.div
layoutId="settingsActiveIndicator"
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-primary rounded-r-full"
initial={false}
transition={{
type: "spring",
stiffness: 500,
damping: 35,
}}
/>
)}
<div
className={cn(
"flex items-center justify-center w-9 h-9 rounded-lg transition-colors",
isActive ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"
)}
>
<Icon className="h-4 w-4" />
</div>
<div className="flex-1 min-w-0">
<p
className={cn(
"text-sm font-medium truncate transition-colors",
isActive ? "text-foreground" : "text-muted-foreground"
)}
>
{item.label}
</p>
<p className="text-xs text-muted-foreground/70 truncate">{item.description}</p>
</div>
<ChevronRight
className={cn(
"h-4 w-4 shrink-0 transition-all",
isActive
? "text-primary opacity-100 translate-x-0"
: "text-muted-foreground/40 opacity-0 -translate-x-1"
)}
/>
</motion.button>
);
})}
</nav>
{/* Footer */}
<div className="p-4 border-t border-border">
<p className="text-xs text-muted-foreground text-center">SurfSense Settings</p>
</div>
</aside>
</>
);
}
function SettingsContent({
activeSection,
searchSpaceId,
onMenuClick,
}: {
activeSection: string;
searchSpaceId: number;
onMenuClick: () => void;
}) {
const activeItem = settingsNavItems.find((item) => item.id === activeSection);
const Icon = activeItem?.icon || Settings;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.4 }}
className="flex-1 min-w-0 h-full overflow-hidden bg-background"
>
<div className="h-full overflow-y-auto">
<div className="max-w-4xl mx-auto p-4 md:p-6 lg:p-10">
{/* Section Header */}
<AnimatePresence mode="wait">
<motion.div
key={activeSection + "-header"}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="mb-6 md:mb-8"
>
<div className="flex items-center gap-3 md:gap-4">
{/* Mobile menu button */}
<Button
variant="outline"
size="icon"
onClick={onMenuClick}
className="md:hidden h-10 w-10 shrink-0"
>
<Menu className="h-5 w-5" />
</Button>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, duration: 0.3 }}
className="flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/10 shadow-sm shrink-0"
>
<Icon className="h-6 w-6 md:h-7 md:w-7 text-primary" />
</motion.div>
<div className="min-w-0">
<h1 className="text-xl md:text-2xl font-bold tracking-tight truncate">
{activeItem?.label}
</h1>
<p className="text-sm text-muted-foreground mt-0.5 truncate">
{activeItem?.description}
</p>
</div>
</div>
</motion.div>
</AnimatePresence>
{/* Section Content */}
<AnimatePresence mode="wait">
<motion.div
key={activeSection}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{
duration: 0.35,
ease: [0.4, 0, 0.2, 1],
}}
>
{activeSection === "models" && <ModelConfigManager searchSpaceId={searchSpaceId} />}
{activeSection === "roles" && <LLMRoleManager searchSpaceId={searchSpaceId} />}
{activeSection === "prompts" && <PromptConfigManager searchSpaceId={searchSpaceId} />}
</motion.div>
</AnimatePresence>
</div>
</div>
</motion.div>
);
}
export default function SettingsPage() {
const router = useRouter();
const params = useParams();
const searchSpaceId = Number(params.search_space_id);
const [activeSection, setActiveSection] = useState("models");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const handleBackToApp = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/researcher`);
}, [router, searchSpaceId]);
return (
<div className="min-h-screen bg-background">
<div className="container max-w-7xl mx-auto p-6 lg:p-8">
<div className="space-y-8">
{/* Header Section */}
<div className="space-y-4">
<div className="flex items-center space-x-4">
{/* Back Button */}
<button
onClick={() => router.push(`/dashboard/${searchSpaceId}`)}
className="flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors"
aria-label="Back to Dashboard"
type="button"
>
<ArrowLeft className="h-5 w-5 text-primary" />
</button>
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<Settings className="h-6 w-6 text-primary" />
</div>
<div className="space-y-1">
<h1 className="text-3xl font-bold tracking-tight">Settings</h1>
<p className="text-lg text-muted-foreground">
Manage your settings for this search space.
</p>
</div>
</div>
<Separator className="my-6" />
</div>
{/* Settings Content */}
<Tabs defaultValue="models" className="space-y-8">
<div className="overflow-x-auto">
<TabsList className="grid w-full min-w-fit grid-cols-3 lg:w-auto lg:inline-grid">
<TabsTrigger value="models" className="flex items-center gap-2 text-sm">
<Bot className="h-4 w-4" />
<span className="hidden sm:inline">Model Configs</span>
<span className="sm:hidden">Models</span>
</TabsTrigger>
<TabsTrigger value="roles" className="flex items-center gap-2 text-sm">
<Brain className="h-4 w-4" />
<span className="hidden sm:inline">LLM Roles</span>
<span className="sm:hidden">Roles</span>
</TabsTrigger>
<TabsTrigger value="prompts" className="flex items-center gap-2 text-sm">
<MessageSquare className="h-4 w-4" />
<span className="hidden sm:inline">System Instructions</span>
<span className="sm:hidden">System Instructions</span>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="models" className="space-y-6">
<ModelConfigManager searchSpaceId={searchSpaceId} />
</TabsContent>
<TabsContent value="roles" className="space-y-6">
<LLMRoleManager searchSpaceId={searchSpaceId} />
</TabsContent>
<TabsContent value="prompts" className="space-y-6">
<PromptConfigManager searchSpaceId={searchSpaceId} />
</TabsContent>
</Tabs>
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="h-full flex bg-background"
>
<SettingsSidebar
activeSection={activeSection}
onSectionChange={setActiveSection}
onBackToApp={handleBackToApp}
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
/>
<SettingsContent
activeSection={activeSection}
searchSpaceId={searchSpaceId}
onMenuClick={() => setIsSidebarOpen(true)}
/>
</motion.div>
);
}