picoclaw/web/frontend/src/components/chat/session-history-menu.tsx
wenjie 87257819f6
feat(web): add restart-required state for default model changes (#1499)
- track boot and config default models in gateway status/events
- preserve running, starting, and restarting states during health checks
- add safer gateway restart handling with stronger backend test coverage
- expose restart-required UI and refresh model state after default model update
2026-03-13 16:30:59 +08:00

109 lines
3.7 KiB
TypeScript

import { IconHistory, IconTrash } from "@tabler/icons-react"
import dayjs from "dayjs"
import type { RefObject } from "react"
import { useTranslation } from "react-i18next"
import type { SessionSummary } from "@/api/sessions"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { ScrollArea } from "@/components/ui/scroll-area"
interface SessionHistoryMenuProps {
sessions: SessionSummary[]
activeSessionId: string
hasMore: boolean
loadError: boolean
loadErrorMessage: string
observerRef: RefObject<HTMLDivElement | null>
onOpenChange: (open: boolean) => void
onSwitchSession: (sessionId: string) => void
onDeleteSession: (sessionId: string) => void
}
export function SessionHistoryMenu({
sessions,
activeSessionId,
hasMore,
loadError,
loadErrorMessage,
observerRef,
onOpenChange,
onSwitchSession,
onDeleteSession,
}: SessionHistoryMenuProps) {
const { t } = useTranslation()
return (
<DropdownMenu onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="sm" className="h-9 gap-2">
<IconHistory className="size-4" />
<span className="hidden sm:inline">{t("chat.history")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-72">
<ScrollArea className="max-h-[300px]">
{loadError && (
<DropdownMenuItem disabled>
<span className="text-destructive text-xs">
{loadErrorMessage}
</span>
</DropdownMenuItem>
)}
{sessions.length === 0 && !loadError ? (
<DropdownMenuItem disabled>
<span className="text-muted-foreground text-xs">
{t("chat.noHistory")}
</span>
</DropdownMenuItem>
) : (
sessions.map((session) => (
<DropdownMenuItem
key={session.id}
className={`group relative my-0.5 flex flex-col items-start gap-0.5 pr-8 ${
session.id === activeSessionId ? "bg-accent" : ""
}`}
onClick={() => onSwitchSession(session.id)}
>
<span className="line-clamp-1 text-sm font-medium">
{session.title || session.preview}
</span>
<span className="text-muted-foreground text-xs">
{t("chat.messagesCount", {
count: session.message_count,
})}{" "}
· {dayjs(session.updated).fromNow()}
</span>
<Button
variant="ghost"
size="icon"
aria-label={t("chat.deleteSession")}
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive absolute top-1/2 right-2 h-6 w-6 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onDeleteSession(session.id)
}}
>
<IconTrash className="h-4 w-4" />
</Button>
</DropdownMenuItem>
))
)}
{hasMore && sessions.length > 0 && (
<div ref={observerRef} className="py-2 text-center">
<span className="text-muted-foreground animate-pulse text-xs">
{t("chat.loadingMore")}
</span>
</div>
)}
</ScrollArea>
</DropdownMenuContent>
</DropdownMenu>
)
}