feat: ChatGPT/Claude-style persistent sidebar for desktop fullscreen chat (v0.57.0)
Cooking assistant's fullscreen mode gets a real left sidebar (always- visible conversation list, hover-reveal rename/delete) on desktop (md+), replacing the small dropdown menu that made sense in the narrow 420px drawer but not in a full-viewport layout. Mobile fullscreen and the normal drawer keep the dropdown (ConversationMenu) — no room for a persistent sidebar there. Extracted the shared fetch/rename/delete/create logic into use-conversation-list.ts so the dropdown (ConversationMenu) and the new sidebar (ConversationSidebar) can't silently diverge — both are thin render layers over the same hook. ConversationMenu gained an optional className prop so the panel can hide it with `md:hidden` specifically when the sidebar is already covering that job. Per-recipe chat is untouched — it's single-threaded by design and never used either of these components. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import type { Conversation } from "./conversation-menu";
|
||||
|
||||
/** Shared fetch/rename/delete logic for the general assistant's conversation
|
||||
* list — used by both the dropdown (ConversationMenu, narrow/mobile) and the
|
||||
* persistent sidebar (desktop fullscreen) skins, so the two never diverge. */
|
||||
export function useConversationList(onDeletedActive: () => void, activeId: string | null) {
|
||||
const t = useTranslations("conversations");
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
|
||||
async function load() {
|
||||
if (loaded) return;
|
||||
setLoaded(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/v1/ai/conversations");
|
||||
if (!res.ok) return;
|
||||
const json = await res.json() as { data: Conversation[] };
|
||||
setConversations(json.data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(): Promise<Conversation | null> {
|
||||
const res = await fetch("/api/v1/ai/conversations", { method: "POST" });
|
||||
if (!res.ok) {
|
||||
toast.error(t("createFailed"));
|
||||
return null;
|
||||
}
|
||||
const created = await res.json() as Conversation;
|
||||
setConversations((prev) => [created, ...prev]);
|
||||
return created;
|
||||
}
|
||||
|
||||
function startRename(conversation: Conversation) {
|
||||
setRenamingId(conversation.id);
|
||||
setRenameValue(conversation.title ?? "");
|
||||
}
|
||||
|
||||
async function submitRename() {
|
||||
if (!renamingId) return;
|
||||
const id = renamingId;
|
||||
const title = renameValue.trim() || null;
|
||||
setRenamingId(null);
|
||||
const res = await fetch(`/api/v1/ai/conversations/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
toast.error(t("renameFailed"));
|
||||
return;
|
||||
}
|
||||
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title } : c)));
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
const id = deleteTargetId;
|
||||
setDeleteTargetId(null);
|
||||
if (!id) return;
|
||||
const res = await fetch(`/api/v1/ai/conversations/${id}`, { method: "DELETE" });
|
||||
if (!res.ok) {
|
||||
toast.error(t("deleteFailed"));
|
||||
return;
|
||||
}
|
||||
setConversations((prev) => prev.filter((c) => c.id !== id));
|
||||
if (id === activeId) onDeletedActive();
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
conversations,
|
||||
loading,
|
||||
load,
|
||||
create,
|
||||
renamingId,
|
||||
renameValue,
|
||||
setRenameValue,
|
||||
startRename,
|
||||
cancelRename: () => setRenamingId(null),
|
||||
submitRename,
|
||||
deleteTargetId,
|
||||
setDeleteTargetId,
|
||||
confirmDelete,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user