import { FileUp, Pencil, Plus, Search, Trash2 } from "lucide-react" import { useParams } from "next/navigation" import { useEffect, useRef, useState } from "react" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { useDictionary } from "@/hooks/use-dictionary" import { formatRelativeTime } from "@/lib/relative-time" import { cn } from "@/lib/utils" import { useChatEngine } from "./chat-engine" /** Title shown for a session, with the default "New Chat" localized */ export function useSessionTitle(): string { const dict = useDictionary() const { currentTitle } = useChatEngine() return !currentTitle || currentTitle === "New Chat" ? dict.workspace.untitled : currentTitle } /** * Popover listing saved diagrams: rename the current one, switch, delete, * start a new one or open a file. */ export function SessionMenu({ children, align = "start", onOpenFile, }: { children: React.ReactNode align?: "start" | "center" | "end" onOpenFile: () => void }) { const dict = useDictionary() const t = dict.sessionHistory const params = useParams<{ lang: string }>() const engine = useChatEngine() const [open, setOpen] = useState(false) const title = useSessionTitle() const [query, setQuery] = useState("") const [renaming, setRenaming] = useState(false) const [draftTitle, setDraftTitle] = useState("") const [toDelete, setToDelete] = useState(null) const renameRef = useRef(null) useEffect(() => { if (!open) { setRenaming(false) setQuery("") } }, [open]) useEffect(() => { if (renaming) renameRef.current?.select() }, [renaming]) // The title as the list shows it const shownTitle = (title: string) => title === "New Chat" ? dict.workspace.untitled : title const sessions = engine.sessions.filter((s) => shownTitle(s.title).toLowerCase().includes(query.trim().toLowerCase()), ) const commitRename = async () => { const next = draftTitle.trim() setRenaming(false) if (next && next !== title) await engine.renameSession(next) } return ( <> {children}
{renaming ? ( setDraftTitle(e.target.value)} onBlur={commitRename} onKeyDown={(e) => { // Enter that picks an IME candidate // (Safari: keyCode 229) if ( e.nativeEvent.isComposing || e.keyCode === 229 ) { return } if (e.key === "Enter") commitRename() if (e.key === "Escape") setRenaming(false) }} maxLength={100} aria-label={t.rename} className="h-8 w-full rounded-lg border border-foreground/20 bg-card px-2.5 text-[13px] font-medium outline-none" /> ) : ( )}
{engine.sessions.length > 0 && (
setQuery(e.target.value)} placeholder={t.searchPlaceholder} className="h-8 w-full rounded-lg bg-muted pr-2.5 pl-8 text-[13px] outline-none placeholder:text-faint" />
)}
{engine.sessions.length === 0 ? (

{t.empty}

) : sessions.length === 0 ? (

{t.noResults}

) : ( sessions.map((session) => { const isCurrent = session.id === engine.currentSessionId // The answer being written must stay in its // own chat const locked = engine.isBusy && !isCurrent const choose = () => { if (locked) return setOpen(false) if (!isCurrent) { engine.selectSession(session.id) } } return ( // biome-ignore lint/a11y/useSemanticElements: contains the delete button
{ if (e.target !== e.currentTarget) return if ( e.key === "Enter" || e.key === " " ) { e.preventDefault() choose() } }} className={cn( "group/session flex cursor-pointer items-center gap-2.5 rounded-lg p-1.5 hover:bg-accent", isCurrent && "bg-accent", locked && "cursor-not-allowed opacity-50 hover:bg-transparent", )} > {session.thumbnailDataUrl && ( // biome-ignore lint/performance/noImgElement: data URL thumbnail )} {shownTitle(session.title)} {formatRelativeTime( session.updatedAt, params.lang, t.justNow, )}
) }) )}
!o && setToDelete(null)} > {t.deleteTitle} {t.deleteDescription} {dict.common.cancel} { if (toDelete) engine.deleteSession(toDelete) setToDelete(null) }} > {dict.common.delete} ) }