Spaces:
Sleeping
Sleeping
| // web/src/components/sidebar/SavedChatSection.tsx | |
| import React, { useEffect, useState } from "react"; | |
| import { Card } from "../ui/card"; | |
| import { Button } from "../ui/button"; | |
| import { Separator } from "../ui/separator"; | |
| import { Bookmark, Trash2, Pencil } from "lucide-react"; | |
| import type { SavedChat } from "../../App"; | |
| export function SavedChatSection({ | |
| isLoggedIn, | |
| savedChats, | |
| onLoadChat, | |
| onDeleteSavedChat, | |
| onRenameSavedChat, | |
| }: { | |
| isLoggedIn: boolean; | |
| savedChats: SavedChat[]; | |
| onLoadChat: (chat: SavedChat) => void; | |
| onDeleteSavedChat: (id: string) => void; | |
| onRenameSavedChat?: (id: string, newTitle: string) => void; | |
| }) { | |
| const [editingId, setEditingId] = useState<string | null>(null); | |
| const [draftTitle, setDraftTitle] = useState<string>(""); | |
| // Keep draft in sync if list changes while editing (e.g., load, delete) | |
| useEffect(() => { | |
| if (!editingId) return; | |
| const current = savedChats.find((c) => c.id === editingId); | |
| if (!current) { | |
| setEditingId(null); | |
| setDraftTitle(""); | |
| return; | |
| } | |
| // Only update draft if it was empty (avoid overwriting user's typing) | |
| if (!draftTitle) setDraftTitle(current.title || ""); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [savedChats, editingId]); | |
| if (!isLoggedIn) return null; | |
| const beginRename = (chat: SavedChat) => { | |
| if (!onRenameSavedChat) return; | |
| setEditingId(chat.id); | |
| setDraftTitle(chat.title || ""); | |
| }; | |
| const cancelRename = () => { | |
| setEditingId(null); | |
| setDraftTitle(""); | |
| }; | |
| const saveRename = () => { | |
| if (!editingId || !onRenameSavedChat) return; | |
| const next = draftTitle.trim(); | |
| const current = savedChats.find((c) => c.id === editingId); | |
| const fallback = current?.title || ""; | |
| // If user clears the title, revert to previous (keeps it clean) | |
| const finalTitle = next || fallback; | |
| if (finalTitle && finalTitle !== current?.title) { | |
| onRenameSavedChat(editingId, finalTitle); | |
| } | |
| setEditingId(null); | |
| setDraftTitle(""); | |
| }; | |
| return ( | |
| // ✅ NO scrolling here. Scrolling is owned by LeftSidebar's scroll container. | |
| <div className="w-full"> | |
| {/* header */} | |
| <div className="px-4 py-3"> | |
| <div className="flex items-center gap-2"> | |
| <Bookmark className="h-4 w-4" /> | |
| <div className="font-semibold">Saved Chat</div> | |
| </div> | |
| </div> | |
| <Separator /> | |
| {/* content (normal flow; NO overflow-y-auto) */} | |
| <div className="px-4 py-4"> | |
| {savedChats.length === 0 ? ( | |
| <Card className="p-8 text-center"> | |
| <div className="text-sm text-muted-foreground">No saved chats yet</div> | |
| <div className="text-xs text-muted-foreground mt-1"> | |
| Save conversations to view them here | |
| </div> | |
| </Card> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {savedChats.map((chat) => { | |
| const isEditing = editingId === chat.id; | |
| const renameEnabled = !!onRenameSavedChat; | |
| return ( | |
| <Card | |
| key={chat.id} | |
| className="p-3 group" | |
| > | |
| <div className="flex items-start justify-between gap-2"> | |
| {/* Left: title + timestamp (click loads, but disabled while editing) */} | |
| <div className="text-left flex-1 min-w-0"> | |
| {!isEditing ? ( | |
| <button | |
| type="button" | |
| onClick={() => onLoadChat(chat)} | |
| className="text-left w-full" | |
| > | |
| <div className="text-sm font-medium leading-5 truncate"> | |
| {chat.title} | |
| </div> | |
| <div className="text-xs text-muted-foreground mt-1"> | |
| {chat.timestamp.toLocaleString()} | |
| </div> | |
| </button> | |
| ) : ( | |
| <div className="w-full"> | |
| <input | |
| autoFocus | |
| value={draftTitle} | |
| onChange={(e) => setDraftTitle(e.target.value)} | |
| onBlur={saveRename} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter") saveRename(); | |
| if (e.key === "Escape") cancelRename(); | |
| }} | |
| className="w-full h-8 px-2 rounded-md border bg-background text-sm outline-none focus:ring-2 focus:ring-ring" | |
| /> | |
| <div className="text-[11px] text-muted-foreground mt-1"> | |
| Enter to save · Esc to cancel | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Right: actions (hover show) */} | |
| <div className="flex items-center gap-1"> | |
| {/* ✏️ Rename */} | |
| {renameEnabled && !isEditing && ( | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity" | |
| onClick={() => beginRename(chat)} | |
| title="Rename" | |
| > | |
| <Pencil className="h-4 w-4" /> | |
| </Button> | |
| )} | |
| {/* 🗑 Delete */} | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity" | |
| onClick={() => onDeleteSavedChat(chat.id)} | |
| title="Delete" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </Card> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |