- {showConversations && (
-
-
-
- )}
-
{messages.map((msg, index) => {
if (msg.speaker === "tool")
return
;
diff --git a/raggr-frontend/src/components/ConversationsPage.tsx b/raggr-frontend/src/components/ConversationsPage.tsx
new file mode 100644
index 0000000..ac200ba
--- /dev/null
+++ b/raggr-frontend/src/components/ConversationsPage.tsx
@@ -0,0 +1,94 @@
+import { useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+import { ArrowLeft, Search } from "lucide-react";
+import { ConversationList } from "./ConversationList";
+import { useConversationBrowser } from "../hooks/useConversationBrowser";
+import type { Conversation } from "../hooks/useConversations";
+import { Input } from "./ui/input";
+import catIcon from "../assets/cat.png";
+
+export const ConversationsPage = () => {
+ const navigate = useNavigate();
+ const {
+ items,
+ search,
+ setSearch,
+ hasMore,
+ loading,
+ loadMore,
+ renameConversation,
+ deleteConversation,
+ } = useConversationBrowser();
+
+ const handleSelect = useCallback(
+ (conversation: Conversation) => navigate(`/c/${conversation.id}`),
+ [navigate],
+ );
+
+ const handleCreateNew = useCallback(() => navigate("/"), [navigate]);
+
+ return (
+
+
+
+
+
+ All conversations
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search conversations..."
+ className="h-10 pl-9"
+ />
+
+
+
+
+
+ {items.length === 0 && !loading && (
+
+ {search ? "No conversations match your search." : "No conversations yet."}
+
+ )}
+
+
+ {hasMore && (
+
+ )}
+
+
+
+ );
+};
diff --git a/raggr-frontend/src/components/SidebarContent.tsx b/raggr-frontend/src/components/SidebarContent.tsx
new file mode 100644
index 0000000..6db33cb
--- /dev/null
+++ b/raggr-frontend/src/components/SidebarContent.tsx
@@ -0,0 +1,128 @@
+import { Link } from "react-router-dom";
+import {
+ Clock,
+ LogOut,
+ PanelLeftClose,
+ Shield,
+ ListTree,
+ X,
+} from "lucide-react";
+import { ConversationList } from "./ConversationList";
+import type { Conversation } from "../hooks/useConversations";
+import catIcon from "../assets/cat.png";
+
+type SidebarContentProps = {
+ conversations: Conversation[];
+ onCreateNewConversation: () => void;
+ onSelectConversation: (conversation: Conversation) => void;
+ onRenameConversation: (id: string, title: string) => void;
+ onDeleteConversation: (id: string) => void;
+ selectedId?: string;
+ isAdmin: boolean;
+ onShowAdmin: () => void;
+ onShowScheduler: () => void;
+ onLogout: () => void;
+ /** Desktop: collapse the sidebar. Renders a PanelLeftClose button. */
+ onCollapse?: () => void;
+ /** Mobile: close the drawer. Renders an X button. */
+ onCloseDrawer?: () => void;
+};
+
+/**
+ * The dark sidebar body shared by the desktop sidebar and the mobile drawer.
+ * Shows the recent conversations, a link to the full list, and admin/account
+ * actions. Exactly one of `onCollapse`/`onCloseDrawer` is expected per usage.
+ */
+export const SidebarContent = ({
+ conversations,
+ onCreateNewConversation,
+ onSelectConversation,
+ onRenameConversation,
+ onDeleteConversation,
+ selectedId,
+ isAdmin,
+ onShowAdmin,
+ onShowScheduler,
+ onLogout,
+ onCollapse,
+ onCloseDrawer,
+}: SidebarContentProps) => {
+ return (
+
+
+
+

+
+ asksimba
+
+
+ {onCollapse && (
+
+ )}
+ {onCloseDrawer && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ See all conversations
+
+ {isAdmin && (
+ <>
+
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/raggr-frontend/src/hooks/useConversationBrowser.ts b/raggr-frontend/src/hooks/useConversationBrowser.ts
new file mode 100644
index 0000000..2b253d2
--- /dev/null
+++ b/raggr-frontend/src/hooks/useConversationBrowser.ts
@@ -0,0 +1,102 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { conversationService } from "../api/conversationService";
+import type { Conversation } from "./useConversations";
+
+const PAGE_SIZE = 20;
+
+/**
+ * Data source for the dedicated /conversations page: server-side search plus
+ * "Load more" pagination. Detects whether more rows exist by asking for one
+ * extra item beyond the page size.
+ */
+export function useConversationBrowser() {
+ const [items, setItems] = useState
([]);
+ const [search, setSearchValue] = useState("");
+ const [hasMore, setHasMore] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ // Guards against a slow earlier request overwriting a newer one.
+ const requestIdRef = useRef(0);
+
+ const fetchPage = useCallback(
+ async (offset: number, searchTerm: string) => {
+ const requestId = ++requestIdRef.current;
+ setLoading(true);
+ try {
+ const fetched = await conversationService.getAllConversations({
+ limit: PAGE_SIZE + 1,
+ offset,
+ search: searchTerm || undefined,
+ });
+ if (requestId !== requestIdRef.current) return;
+
+ const page = fetched
+ .slice(0, PAGE_SIZE)
+ .map((c) => ({ id: c.id, title: c.name }));
+ setHasMore(fetched.length > PAGE_SIZE);
+ setItems((prev) => (offset === 0 ? page : [...prev, ...page]));
+ } catch (err) {
+ if (requestId === requestIdRef.current) {
+ console.error("Failed to load conversations:", err);
+ }
+ } finally {
+ if (requestId === requestIdRef.current) setLoading(false);
+ }
+ },
+ [],
+ );
+
+ // Debounce search changes; reset to the first page on each new term.
+ useEffect(() => {
+ const handle = setTimeout(() => fetchPage(0, search), 250);
+ return () => clearTimeout(handle);
+ }, [search, fetchPage]);
+
+ const loadMore = useCallback(() => {
+ if (loading || !hasMore) return;
+ fetchPage(items.length, search);
+ }, [loading, hasMore, items.length, search, fetchPage]);
+
+ const renameConversation = useCallback(
+ async (id: string, title: string) => {
+ const trimmed = title.trim();
+ if (!trimmed) return;
+ const previous = items;
+ setItems((prev) =>
+ prev.map((c) => (c.id === id ? { ...c, title: trimmed } : c)),
+ );
+ try {
+ await conversationService.renameConversation(id, trimmed);
+ } catch (err) {
+ console.error("Failed to rename conversation:", err);
+ setItems(previous);
+ }
+ },
+ [items],
+ );
+
+ const deleteConversation = useCallback(
+ async (id: string) => {
+ const previous = items;
+ setItems((prev) => prev.filter((c) => c.id !== id));
+ try {
+ await conversationService.deleteConversation(id);
+ } catch (err) {
+ console.error("Failed to delete conversation:", err);
+ setItems(previous);
+ }
+ },
+ [items],
+ );
+
+ return {
+ items,
+ search,
+ setSearch: setSearchValue,
+ hasMore,
+ loading,
+ loadMore,
+ renameConversation,
+ deleteConversation,
+ };
+}
diff --git a/raggr-frontend/src/hooks/useConversations.ts b/raggr-frontend/src/hooks/useConversations.ts
index 78a2f0f..99b2027 100644
--- a/raggr-frontend/src/hooks/useConversations.ts
+++ b/raggr-frontend/src/hooks/useConversations.ts
@@ -7,6 +7,10 @@ export type Conversation = {
id: string;
};
+// The sidebar/drawer only shows the most recent conversations; the full list
+// lives on the dedicated /conversations page.
+const SIDEBAR_LIMIT = 10;
+
export function useConversations() {
const navigate = useNavigate();
const { conversationId } = useParams();
@@ -14,7 +18,9 @@ export function useConversations() {
const refreshConversations = useCallback(async () => {
try {
- const fetched = await conversationService.getAllConversations();
+ const fetched = await conversationService.getAllConversations({
+ limit: SIDEBAR_LIMIT,
+ });
setConversations(fetched.map((c) => ({ id: c.id, title: c.name })));
} catch (err) {
console.error("Failed to load conversations:", err);