09dd983c4e
Rework mobile navigation into a proper left slide-in drawer with a dimmed backdrop, sharing a new SidebarContent component with the desktop sidebar so mobile users can reach Admin/Scheduler/Sign out. The sidebar now loads only the 10 most recent conversations; a dedicated /conversations page browses the full list with server-side search and "Load more" pagination. - Backend: limit/offset/search query params on GET /api/conversation/ - SidebarContent shared by desktop sidebar and mobile drawer - useConversationBrowser hook powers the dedicated page - Remove duplicated inline conversation lists from ChatScreen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { conversationService } from "../api/conversationService";
|
|
|
|
export type Conversation = {
|
|
title: string;
|
|
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();
|
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
|
|
const refreshConversations = useCallback(async () => {
|
|
try {
|
|
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);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refreshConversations();
|
|
}, [refreshConversations]);
|
|
|
|
// The active conversation is derived from the URL. Fall back to a minimal
|
|
// object while the list is still loading (e.g. on a deep-link refresh).
|
|
const selectedConversation = useMemo<Conversation | null>(() => {
|
|
if (!conversationId) return null;
|
|
return (
|
|
conversations.find((c) => c.id === conversationId) ?? {
|
|
id: conversationId,
|
|
title: "",
|
|
}
|
|
);
|
|
}, [conversationId, conversations]);
|
|
|
|
const createConversation = useCallback(async (): Promise<Conversation> => {
|
|
const newConv = await conversationService.createConversation();
|
|
const conversation = { title: newConv.name, id: newConv.id };
|
|
setConversations((prev) => [conversation, ...prev]);
|
|
navigate(`/c/${conversation.id}`);
|
|
return conversation;
|
|
}, [navigate]);
|
|
|
|
const renameConversation = useCallback(
|
|
async (id: string, title: string): Promise<void> => {
|
|
const trimmed = title.trim();
|
|
if (!trimmed) return;
|
|
// Optimistic update, roll back on failure. selectedConversation is
|
|
// derived from `conversations`, so it updates automatically.
|
|
const previous = conversations;
|
|
setConversations((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);
|
|
setConversations(previous);
|
|
}
|
|
},
|
|
[conversations],
|
|
);
|
|
|
|
const deleteConversation = useCallback(
|
|
async (id: string): Promise<void> => {
|
|
const previous = conversations;
|
|
const wasActive = conversationId === id;
|
|
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
// Navigating home clears the message view via the route change.
|
|
if (wasActive) navigate("/");
|
|
try {
|
|
await conversationService.deleteConversation(id);
|
|
} catch (err) {
|
|
console.error("Failed to delete conversation:", err);
|
|
setConversations(previous);
|
|
}
|
|
},
|
|
[conversations, conversationId, navigate],
|
|
);
|
|
|
|
return {
|
|
conversations,
|
|
selectedConversation,
|
|
createConversation,
|
|
renameConversation,
|
|
deleteConversation,
|
|
refreshConversations,
|
|
};
|
|
}
|