Delete Sessions
Now that we are on the final stage of the project, this is where we will be adding a feature to delete the sessions right from the UI and from MongoDB as well.
Add Delete Session function
Now, we will add a function to delete the sessions from the UI and from MongoDB as well.
export async function deleteChatSession(sessionId: string) {
const client = await clientPromise;
const db = client.db("secondbrain");
// Delete all messages for this session
await db.collection("messages").deleteMany({
sessionId: new ObjectId(sessionId),
});
// Delete the session itself
await db.collection("sessions").deleteOne({
_id: new ObjectId(sessionId),
});
}Update the messeges per session to have the DELETE api call to delete messages from the given session.
export async function DELETE(_: NextRequest, props: { params: Promise<{ sessionId: string }> }) {
const params = await props.params;
try {
await deleteChatSession(params.sessionId);
return Response.json({ success: true });
} catch (error) {
console.error("Failed to delete chat session:", error);
return Response.json({ error: "Failed to delete chat session" }, { status: 500 });
}
}Update the Sidebar
Now we shall add the delete button on the sidebar and update the contents on the sidebar when any session is deleted and also make it collapsable.
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { Button } from "./ui/Button";
import { Plus, MessageSquare, Brain, Trash2, ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "framer-motion";
export default function ChatSidebar() {
const [sessions, setSessions] = useState<any[]>([]);
const [isCollapsed, setIsCollapsed] = useState(false);
const pathname = usePathname();
const router = useRouter();
const fetchSessions = async () => {
try {
const res = await fetch("/api/sessions");
const data = await res.json();
if (Array.isArray(data)) {
setSessions(data);
}
} catch (error) {
console.error("Failed to fetch sessions:", error);
}
};
useEffect(() => {
fetchSessions();
// Listen for internal updates (e.g. from the chat page)
const handleUpdate = () => fetchSessions();
window.addEventListener("chat-updated", handleUpdate);
return () => window.removeEventListener("chat-updated", handleUpdate);
}, []);
const handleDeleteSession = async (e: React.MouseEvent, id: string) => {
e.preventDefault();
e.stopPropagation();
if (!confirm("Are you sure you want to delete this chat session?")) return;
try {
const res = await fetch(`/api/messages/${id}`, { method: "DELETE" });
if (res.ok) {
setSessions((prev) => prev.filter((s) => s._id !== id));
if (pathname.includes(id)) {
router.push("/chat");
}
// Notify other components (if any)
window.dispatchEvent(new CustomEvent("chat-updated"));
}
} catch (error) {
console.error("Failed to delete session:", error);
}
};
return (
<motion.aside
initial={false}
animate={{ width: isCollapsed ? 64 : 288 }}
className="h-screen border-r border-white/5 bg-black flex flex-col relative shrink-0 overflow-hidden"
>
{/* Collapse Toggle */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute -right-3 top-20 w-6 h-6 rounded-full bg-zinc-900 border border-white/10 flex items-center justify-center text-zinc-500 hover:text-white z-50 transition-colors"
title={isCollapsed ? "Expand Sidebar" : "Collapse Sidebar"}
>
{isCollapsed ? <ChevronRight className="w-3 h-3" /> : <ChevronLeft className="w-3 h-3" />}
</button>
{/* Header */}
<div className={cn("p-6", isCollapsed && "px-4")}>
<Link href="/" className="flex items-center gap-2 mb-8 group">
<div className="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center group-hover:rotate-12 transition-transform shrink-0">
<Brain className="w-5 h-5 text-white" />
</div>
{!isCollapsed && <span className="font-bold text-lg tracking-tight truncate">Second Brain</span>}
</Link>
<Link href="/chat">
<Button variant="primary" className={cn("w-full justify-start gap-2 h-11 rounded-xl shadow-blue-500/10", isCollapsed && "px-0 justify-center")}>
<Plus className="w-4 h-4 shrink-0" />
{!isCollapsed && <span>New Chat</span>}
</Button>
</Link>
</div>
{/* Sessions List */}
<div className="flex-1 overflow-y-auto px-3 space-y-1">
<div className={cn("px-3 mb-2", isCollapsed && "px-0 flex justify-center")}>
{!isCollapsed ? (
<span className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">Recent Chats</span>
) : (
<div className="h-px bg-white/5 w-8" />
)}
</div>
{sessions.length === 0 ? (
!isCollapsed && (
<div className="px-3 py-4 text-xs text-zinc-500 italic">
No recent sessions
</div>
)
) : (
sessions.map((s) => {
const isActive = pathname.includes(s._id);
return (
<Link
key={s._id}
href={`/chat/${s._id}`}
className={cn(
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all relative overflow-hidden",
isActive ? "bg-white/10 text-white" : "text-zinc-400 hover:text-white hover:bg-white/5",
isCollapsed && "justify-center px-0"
)}
>
<MessageSquare className={cn("w-4 h-4 shrink-0 opacity-50 group-hover:opacity-100", isActive && "opacity-100 text-blue-400")} />
{!isCollapsed && (
<>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{s.title || "Untitled Chat"}</div>
<div className="text-[10px] opacity-40 mt-0.5">
{new Date(s.updatedAt).toLocaleDateString([], { month: 'short', day: 'numeric' })}
</div>
</div>
<button
onClick={(e) => handleDeleteSession(e, s._id)}
className="opacity-0 group-hover:opacity-100 p-1.5 rounded-lg hover:bg-red-500/20 text-zinc-500 hover:text-red-400 transition-all"
title="Delete Chat"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</>
)}
</Link>
);
})
)}
</div>
</motion.aside>
);
}Update chat pages to dynamically update the sidebar
This API will help us store the current session to MongoDB
"use client";
import { Brain, Mic, Send, Pause } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import "regenerator-runtime/runtime";
import SpeechRecognition, { useSpeechRecognition } from "react-speech-recognition";
import { useRouter } from "next/navigation";
import { Button } from "@/app/components/ui/Button";
export default function ChatPage() {
const [input, setInput] = useState("");
const [isMounted, setIsMounted] = useState(false);
const [loading, setLoading] = useState(false);
const router = useRouter();
useEffect(() => {
setIsMounted(true);
}, []);
const {
transcript,
listening,
resetTranscript,
browserSupportsSpeechRecognition
} = useSpeechRecognition();
// Effect to append transcript when listening stops
const isAppending = useRef(false);
useEffect(() => {
if (!listening && transcript) {
if (isAppending.current) return;
isAppending.current = true;
setInput((prev) => (prev ? prev + " " + transcript : transcript));
resetTranscript();
setTimeout(() => { isAppending.current = false; }, 100);
}
}, [listening, transcript, resetTranscript]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || loading) return;
setLoading(true);
const res = await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ firstMessage: input }),
});
const { sessionId } = await res.json();
// Notify sidebar to refresh
window.dispatchEvent(new CustomEvent("chat-updated"));
// Redirect with first message
router.push(
`/chat/${sessionId}?q=${encodeURIComponent(input)})`
);
};
function toggleListening() {
if (listening) {
SpeechRecognition.stopListening();
} else {
SpeechRecognition.startListening({ continuous: true });
}
}
if (!isMounted) {
return (
<div className="flex flex-col h-full items-center justify-center p-8 bg-[#0a0a0a] animate-pulse">
<div className="w-20 h-20 rounded-3xl bg-zinc-900 border border-white/5" />
</div>
);
}
if (!browserSupportsSpeechRecognition) {
return (
<div className="flex h-full items-center justify-center p-8 bg-[#0a0a0a] text-zinc-500">
Browser doesn't support speech recognition.
</div>
);
}
if (typeof window !== "undefined" && !("webkitSpeechRecognition" in window)) {
console.warn("Voice input not supported in this browser");
}
return (
<div className="flex flex-col h-full items-center justify-center p-8 text-center bg-[#0a0a0a]">
<div className="w-20 h-20 rounded-3xl bg-zinc-900 border border-white/5 flex items-center justify-center mb-6 shadow-2xl">
<Brain className="w-10 h-10 text-blue-500/50" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">
Start a Conversation
</h2>
<p className="text-zinc-500 max-w-xs text-sm leading-relaxed mb-6">
Ask a question to begin exploring your knowledge base.
</p>
{/* Input */}
<form
onSubmit={handleSubmit}
className="w-full max-w-md relative"
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask your second brain..."
disabled={loading}
className="
w-full pr-14 py-4 px-5
rounded-2xl bg-zinc-900/50
border border-white/10
focus:border-blue-500/50
focus:ring-blue-500/20
shadow-xl
text-sm
"
/>
<div className="absolute right-14 top-2">
<Button onClick={toggleListening} type="button" size="icon" className={`h-10 w-10 rounded-xl ${listening ? "bg-red-600 animate-pulse" : ""}`}>
{listening ? <Pause className="w-4 h-4 text-white" /> : <Mic className="w-4 h-4 text-white" />}
</Button>
</div>
<Button
type="submit"
disabled={loading || !input.trim()}
className="absolute right-2 top-2 h-10 w-10 rounded-xl cursor-pointer"
>
<Send className="w-4 h-4 bg-white" />
</Button>
</form>
</div>
);
} "use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState, useRef, useEffect } from "react";
import "regenerator-runtime/runtime";
import SpeechRecognition, { useSpeechRecognition } from "react-speech-recognition";
import Markdown from "react-markdown";
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import TypingIndicator from "@/app/components/TypingIndicator";
import { Button } from "@/app/components/ui/Button";
import { Input } from "@/app/components/ui/Input";
import { Send, User, Bot, Brain, Mic2, Mic, Pause, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { use } from "react";
import { useSearchParams } from "next/navigation";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import { markdownSchema } from "@/lib/markdownSanitizer";
import Image from "next/image";
import { Suspense } from "react";
function transformYoutubeLinks(text: string): string {
const youtubeRegex = /(?:https?://)?(?:www.)?(?:youtube.com/(?:[^/
s]+/S+/|(?:v|e(?:mbed)?)/|S*?[?&]v=)|youtu.be/)([a-zA-Z0-9_-]{11})/g;
return text.replace(youtubeRegex, (match, videoId) => {
return `<iframe src="https://www.youtube.com/embed/${videoId}" allowfullscreen></iframe>`;
});
}
function ChatContent({ id }: { id: string }) {
const router = useRouter();
const [input, setInput] = useState("");
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const { messages, setMessages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: `/api/chat?sessionId=${id}`
}),
body: {
sessionId: id
}
} as any);
const [historyLoaded, setHistoryLoaded] = useState(false);
const {
transcript,
listening,
resetTranscript,
browserSupportsSpeechRecognition
} = useSpeechRecognition();
// Effect to append transcript when listening stops
const isAppending = useRef(false);
useEffect(() => {
if (!listening && transcript) {
if (isAppending.current) return;
isAppending.current = true;
setInput((prev) => (prev ? prev + " " + transcript : transcript));
resetTranscript();
setTimeout(() => { isAppending.current = false; }, 100);
}
}, [listening, transcript, resetTranscript]);
useEffect(() => {
async function fetchHistory() {
try {
const res = await fetch(`/api/messages/${id}`);
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
const mappedMessages = data.map((m: any) => ({
id: m._id,
role: m.role as any,
parts: [{ type: "text" as const, text: m.content }],
}));
setMessages(mappedMessages);
}
setHistoryLoaded(true);
} catch (error) {
console.error("Failed to fetch history:", error);
setHistoryLoaded(true);
}
}
fetchHistory();
}, [id, setMessages]);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || status === "submitted" || status === "streaming") return;
await sendMessage({ text: input, metadata: { sessionId: id } });
setInput("");
};
const isLoading = status === "submitted" || status === "streaming";
const searchParams = useSearchParams();
const firstQuery = searchParams.get("q");
const sentRef = useRef(false);
useEffect(() => {
if (historyLoaded && firstQuery && !sentRef.current && messages.length === 0) {
sentRef.current = true;
sendMessage({
text: firstQuery,
metadata: { sessionId: id }
})
}
}, [historyLoaded, firstQuery, sendMessage, messages.length, id]);
const handleDeleteChat = async () => {
if (!confirm("Are you sure you want to delete this chat session? This action cannot be undone.")) {
return;
}
try {
const res = await fetch(`/api/messages/${id}`, {
method: "DELETE",
});
if (res.ok) {
window.dispatchEvent(new CustomEvent("chat-updated"));
router.push("/");
} else {
alert("Failed to delete chat.");
}
} catch (error) {
console.error("Error deleting chat:", error);
alert("An error occurred while deleting the chat.");
}
};
function toggleListening() {
if (listening) {
SpeechRecognition.stopListening();
} else {
SpeechRecognition.startListening({ continuous: true });
}
}
if (!isMounted) {
return (
<div className="flex flex-col h-screen bg-[#0a0a0a] animate-pulse">
<header className="h-16 border-b border-white/5 flex items-center px-8 bg-black/50 backdrop-blur-xl">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-800" />
<div className="h-4 w-32 bg-zinc-800 rounded" />
</div>
</header>
<main className="flex-1" />
</div>
);
}
if (!browserSupportsSpeechRecognition) {
return (
<div className="flex h-screen items-center justify-center bg-[#0a0a0a] text-zinc-500">
Voice input not supported in this browser.
</div>
);
}
if (typeof window !== "undefined" && !("webkitSpeechRecognition" in window)) {
console.warn("Voice input not supported in this browser");
}
return (
<div className="flex flex-col h-screen bg-[#0a0a0a]">
{/* Header */}
<header className="h-16 border-b border-white/5 flex items-center justify-between px-8 bg-black/50 backdrop-blur-xl shrink-0">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-800 flex items-center justify-center border border-white/10">
<Brain className="w-4 h-4 text-blue-400" />
</div>
<div>
<h1 className="text-sm font-semibold text-white">Knowledge Session</h1>
<p className="text-[10px] text-zinc-500 uppercase tracking-widest">ID: {id.slice(-6)}</p>
</div>
</div>
<Button
onClick={handleDeleteChat}
variant="ghost"
size="icon"
className="text-zinc-500 hover:text-red-400 hover:bg-red-400/10 transition-colors"
title="Delete Chat"
>
<Trash2 className="w-4 h-4" />
</Button>
</header>
{/* Messages */}
<main className="flex-1 overflow-y-auto px-4 md:px-0 py-8">
<div className="max-w-3xl mx-auto space-y-8 pb-12">
{messages.length === 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col items-center justify-center py-20 text-center"
>
<div className="w-16 h-16 rounded-3xl bg-blue-600/10 border border-blue-500/20 flex items-center justify-center mb-6">
<Brain className="w-8 h-8 text-blue-500" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">Deep Knowledge Retrieval</h2>
<p className="text-zinc-500 max-w-sm">
This session is ready. Ask anything about your indexed documents and notes.
</p>
</motion.div>
)}
<AnimatePresence initial={false}>
{messages.map((m) => (
<motion.div
key={m.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`flex gap-4 ${m.role === "user" ? "flex-row-reverse" : "flex-row"}`}
>
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 border ${m.role === "user"
? "bg-blue-600/10 border-blue-500/20 text-blue-400"
: "bg-zinc-800 border-white/10 text-zinc-400"
}`}>
{m.role === "user" ? <User className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
</div>
<div className={`flex flex-col max-w-[85%] ${m.role === "user" ? "items-end" : "items-start"}`}>
<div className={`px-5 py-3 rounded-2xl ${m.role === "user"
? "bg-blue-600 text-white"
: "bg-zinc-900 border border-white/5 text-zinc-200"
}`}>
{m.parts.map((p, i) => (
p.type === "text" ? (
<div key={i} className="prose prose-sm prose-invert max-w-none">
{p.text.split("
").map((block, idx) => {
return (
<Markdown
rehypePlugins={[
rehypeRaw,
[rehypeSanitize, markdownSchema]
]}
key={idx}
components={{
code({ inline, className, children, ...props }: any) {
const match = /language-(w+)/.exec(className || "");
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
className="rounded-xl my-4 text-xs"
{...props}
>
{String(children).replace(/
$/, "")}
</SyntaxHighlighter>
) : (
<code
className="bg-white/10 px-1.5 py-0.5 rounded text-sm"
{...props}
>
{children}
</code>
);
},
iframe({ src }) {
if (!src) return null;
return (
<div className="my-4 w-full overflow-hidden rounded-xl border border-white/10 bg-black">
<div className="relative w-full" style={{ paddingTop: "56.25%" }}>
<iframe
src={src}
title="Embedded video"
className="absolute top-0 left-0 w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
referrerPolicy="strict-origin-when-cross-origin"
/>
</div>
</div>
);
},
img({ src, alt }) {
if (!src) return null;
return (
<div className="group relative my-8 overflow-hidden rounded-2xl border border-white/10 bg-zinc-900 shadow-2xl transition-all duration-300 hover:border-white/20">
<Image
src={(src as string).replace("/public", "")}
alt={alt ?? "Knowledge Image"}
className="h-auto w-full transition-transform duration-700 ease-out group-hover:scale-[1.02]"
loading="lazy"
width={1600}
height={900}
quality={100}
/>
</div>
);
},
p({ children }) {
return <div className="my-3">{children}</div>;
},
}}
>
{transformYoutubeLinks(block)}
</Markdown>
);
})}
</div>
) : null
))}
</div>
<div className="mt-1 px-2 text-[10px] text-zinc-600">
{m.role === "user" ? "You" : "Second Brain"}
</div>
</div >
</motion.div >
))
}
</AnimatePresence >
{isLoading && (
<div className="flex gap-4">
<div className="w-8 h-8 rounded-full bg-zinc-800 border border-white/10 flex items-center justify-center text-zinc-400">
<Bot className="w-4 h-4" />
</div>
<div className="bg-zinc-900/50 border border-white/5 rounded-2xl px-4 py-2">
<TypingIndicator />
</div>
</div>
)}
<div ref={bottomRef} className="h-4" />
</div >
</main >
{/* Input Area */}
< div className="p-6 bg-linear-to-t from-black via-black to-transparent" >
<form onSubmit={onSubmit} className="max-w-3xl mx-auto relative group">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Search your brain..."
disabled={isLoading}
className="pr-14 h-14 rounded-2xl bg-zinc-900/50 border-white/10 focus:border-blue-500/50 focus:ring-blue-500/20 shadow-2xl transition-all"
/>
<div className="absolute right-14 top-2">
<Button onClick={toggleListening} type="button" size="icon" className={`h-10 w-10 rounded-xl cursor-pointer ${listening ? "bg-red-600 animate-pulse" : ""}`}>
{listening ? <Pause className="w-4 h-4 text-white" /> : <Mic className="w-4 h-4 text-white" />}
</Button>
</div>
<div className="absolute right-2 top-2">
<Button
type="submit"
size="icon"
disabled={isLoading || !input.trim()}
className="h-10 w-10 rounded-xl cursor-pointer"
>
<Send className="w-4 h-4" />
</Button>
</div>
</form>
<p className="text-center mt-3 text-[10px] text-zinc-600 tracking-wider">
AI generated responses may be inaccurate. Check citations for verification.
</p>
</div >
</div >
);
}
export default function ChatSessionPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params);
return (
<Suspense fallback={<div className="flex h-screen items-center justify-center bg-[#0a0a0a] text-zinc-500">Loading session...</div>}>
<ChatContent id={id} />
</Suspense>
);
}You Did It! 🥳
Congratulations! You've successfully built your very own custom ChatGPT-like application. From persistent session management to a real-time streaming interface, you've mastered the core components of modern AI chat platforms.
Watch the final project wrap-up and demo here:
